Creating Live Stock Quote Sample Applications using the Message service You can programmatically create a Rich Internet Application (RIA) that displays stock data by using LiveCycle Data Services. You use the Flex client-side API to create the client application. You use the Java API to create server-side classes that retrieve financial data from a content provider and sends messages to a destination. When the client application detects new messages, the application is updated in real time with financial data. For example, assume that the client application tracks Adobe stock. When the server retrieves updated data from the financial content provider, it creates an updated message and pushes the message to the destination. The client application detects the new message and displays changes to the stock price. The following illustration shows the stock quote application that is created in this development article. Creating Live Stock Quote Sample Applications using the Message service The stock quote application displays data related to the stock symbol that a user entered. In the previous illustration, notice that the Adobe stock price is displayed. This application displays the following data: The first section displays current stock activity within a data grid control. The data grid control displays information such as the stock name, its symbol, the change in the price, and so on. The second section displays daily stock activity within a line chart control. As a stock price either moves up or down, the activity is plotted within this control. The user can modify the zoom perspective by adjusting the slider control. The third section displays stock history information within an area chart control. The user can view different ranges by clicking different time range buttons. For example, the user can view the stock activity over the past year by clicking the 1y button. LiveCycle Data Services retrieves stock data that is displayed within this client application. The following illustration shows LiveCycle Data Services retrieving stock data from a third party financial content provider. LiveCycle Data Services retrieves stock data from the content provider by using a Java API. As the server receives stock data, it creates messages and sends the messages to a destination. A client application written using Flash Builder can receive messages from a destination and display message data. In this workflow, LiveCycle Data Services is a message producer and the client applications are message consumers. Creating Live Stock Quote Sample Applications using the Message service Note: For more information about message consumers and message producers, see the Messaging Service section in Using Adobe LiveCycle Data Services Guide. For this development article, the financial content provider that is used is Yahoo Finance. This service returns financial data within a comma delimited format, as shown in the following example: Date,Open,High,Low,Close,Volume,Adj Close 2010-0812,27.80,28.33,27.76,28.08,7694000,28 The Java server classes download stock data from Yahoo Finance and then parses the comma delimited data. The data is placed within messages and sent to the destination located on the server. Each stock has its own subtopic within the destination. A subtopic is a specific category within a destination. For example, the ADBE subtopic tracks information about the Adobe stock. Note: The free Yahoo Finance data provider only returns data when the financial markets are open from 9:30 to 4 pm EST. If you are following along with this development article, keep this issue in mind. Also, the data that is displayed is subject to a 15-minute delay. That is, the client application displays data that is 15 minutes old. To create the stock quote application by using LiveCycle Data Services, perform the following tasks: 1. Configure the destinations on the server. 2. Create the server-side classes. 3. Deploy the stock quote application server classes. 4. Create the client application using Flash Builder. 5. Create the JSP that starts the server thread. Note: This development article contains all of the Java and ActionScript logic that is required to create this application. The code is explained in the corresponding sections. For example, if you are interested in knowing how to create the server-side classes, then read the section titled Create the server-side classes. However, if you want to develop this application, then it is recommended that you read tasks 1-5. Creating Live Stock Quote Sample Applications using the Message service Configure the destinations on the server The stock quote application uses both the Message service and the RPC service. The Message service is used to retrieve real-time stock information that is displayed in the data grid control and the linechart control. The RPC service is used to retrieve historical data and stock quote prices that are displayed in the area chart control. As a result, you define the following three destinations in XML configuration files: stockFeed - this destination provides real-time financial information for the stock. This destination is a Message service destination and is defined in the messagingconfig.xml file. setQuotes - this destination is used to set which stock price is displayed. This destination is an RPC service destination and is defined in the remoting-config.xml file. getStockHistory - this destination is used to retrieve historical information for the stock. This destination is an RPC service destination and is defined in the remotingconfig.xml file. These XML configuration files are located in the following folder: [Install directory]\lcds\tomcat\webapps\[web application name]\WEB-INF\flex where [Install directory] is the directory on which LiveCycle Data Services is installed and [web application name] is your web application name. For example, if the name of your web application is stockDashboard, place these files in the following folder: [Install directory]\lcds\tomcat\webapps\stockDashboard\WEB-INF\flex Modifying the messaging-config.xml file In the messaging-config.xml file, define a destination named stockFeed by using the destination tag. Both the Java server class and the client application created using Flash Builder references this destination. The server pushes messages (changes to a stock price) to this destination and the client retrieves messages from this destination. The following XML code represents the stockFeed destination. <destination id="stockFeed"> <properties> Creating Live Stock Quote Sample Applications using the Message service <network> <session-timeout>0</session-timeout> </network> <server> <message-time-to-live>0</message-time-to-live> <allow-subtopics>true</allow-subtopics> <subtopic-separator>.</subtopic-separator> </server> </properties> </destination> The id attribute defines the name of the destination. In this example, the name of the destination is stockFeed. The allow-subtopics attribute enables the server (the message producer) to send messages to a specific category, called a subtopic, within the stockFeed destination. The client application retrieves messages from just that subtopic. As a result, only part of a client application is updated, such as a specific data grid, not the entire client application. The subtopic-separators attribute lets you change the separator character. The default value is a period character (.). The following XML code represents the entire messagingconfig.xml file for the stock quote application. Notice that the default channel is my-rtmp. Note: The my-rtmp channel is only supported by LiveCycle Data Services. If you are using BlazeDS, you can use Streaming-AMF. For information about channels, see the Using Adobe LiveCycle Data Services ES2 guide. <?xml version="1.0" encoding="UTF-8"?> <service id="message-service" class="flex.messaging.services.MessageService"> <adapters> <adapter-definition id="actionscript" class="flex.messaging.services.messaging.adapters.ActionScriptAdapter" default="true" /> <adapter-definition id="jms" class="flex.messaging.services.messaging.adapters.JMSAdapter"/> </adapters> <default-channels> <channel ref="my-rtmp"/> </default-channels> <destination id="stockFeed"> <properties> Creating Live Stock Quote Sample Applications using the Message service <network> <session-timeout>0</session-timeout> </network> <server> <message-time-to-live>0</message-time-to-live> <allow-subtopics>true</allow-subtopics> <subtopic-separator>.</subtopic-separator> </server> </properties> </destination> </service> Modifying the remoting-config.xml file In the remoting-config.xml file, define two destinations named setQuotes and getStockHistory by using the destination tag. Both these destinations reference the Java server classes that are created within this development article. These destinations let objects defined in the client application to call methods defined in Java server classes. That is, an ActionScript object can call a Java method that is located on the server. The following XML code represents the entire remoting-config.xml file for the stock quote application. (The bolded code represents the new destinations.) <?xml version="1.0" encoding="UTF-8"?> <service id="remoting-service" class="flex.messaging.services.RemotingService"> <adapters> <adapter-definition id="java-object" class="flex.messaging.services.remoting.adapters.JavaAdapter" default="true"/> </adapters> <default-channels> <channel ref="my-amf"/> </default-channels> <destination id="setQuotes"> <properties> <source>httpDownloader.RealTimeHttpDownloader</source> </properties> </destination> <destination id="getStockHistory"> <properties> <source>httpDownloader.HistoryHttpDownloader</source> Creating Live Stock Quote Sample Applications using the Message service </properties> </destination> </service> The setQuotes destination references a Java class named httpDownloader.RealTimeHttpDownloader. Likewise, the getStockHistory destination references a Java class named httpDownloader.HistoryHttpDownloader. Both of these class return data from the financial content provider. For information about creating these classes, see Create the server-side classes. Create the server-side classes The LiveCycle Data Services stock quote application consists of server-side classes that download financial information from Yahoo finance. These classes creates messages that contain financial data, and pushes the messages to the stockFeed destination. You can create these classes in a Java IDE such as Eclipse. The stock quote application consists of the following server-side classes: StockQuoteFeed - Creates a StockQuoteFeedThread instance and starts the thread. A JSP references this class to start the server thread. (See Create the JSP that starts the server thread.) StockQuoteFeedThread - Creates a server thread that produces message and pushes the messages to the stockFeed destination. HttpDownloader - Represents the base download class that is able to download financial data from Yahoo finance. RealTimeHttpDownloader - Represents the class that downloads real-time stock information. Real-time information populates the data grid control located in the client application. HistoryHttpDownloader - Represents the class that downloads stock history information. History information populates the area control located in the client application. Stock - Represents a stock that contains data members that store information such as high value, low value, open value, and so on. A Stock instance is the complex Creating Live Stock Quote Sample Applications using the Message service Java type that is used as the body of the message that is sent to the stockFeed destination. StockInTime - Represents a save each point of data in a stock's history. The date and close data members are used as (x,y) coordinates that can be plotted in the line chart control located in the client application. LiveCycle Data Services Library Files In your Java server project, include LiveCycle Data Services JAR files in your project’s class path. Ensure that you include LiveCycle Data Services JAR files located in the following directory: [Install directory]\lcds\tomcat\webapps\lcds-samples\WEB-INF\lib where [Install directory] is the directory on which LiveCycle Data Services is installed. Understanding the StockQuoteFeed class The StockQuoteFeed class starts the thread on the server that downloads financial data from the content provider, creates messages, and sends them to the stockFeed destination. The StockQuoteFeed class contains a private static data member named thread of type StockQuoteFeedThread, as shown in the following example. private static StockQuoteFeedThread thread ; This thread represents the server thread. This StockQuoteFeed class contains a static method named main that creates a StockQuoteFeed class (an instance of itself) and calls the start method. Within the start method, the thread data member is checked to see if it is null. If this data member is null, then memory is allocated to the thread data member and its start method is invoked. The following code example represents the start method that belongs to the StockQuoteFeed class. This method is called from the JSP that starts the server thread. (See Create the JSP that starts the server thread.) //If thread is null, allocate memory to thread, and call its start method public void start() { if (thread == null) { thread = new StockQuoteFeedThread(); thread.start(); } Creating Live Stock Quote Sample Applications using the Message service } Understanding the StockQuoteFeedThread class The StockQuoteFeedThread class extends java.lang.thread and contains a method named run. The run method performs the following tasks: Checks to see if the thread is running. By default, the thread is in a running state. Checks to see if the client application has request information about a specific stock. If a client application has not requested information about a stock, nothing happens and the thread remains in a running state. If a client application has requested information about a specific stock, the thread starts to create a message. (Proceed to the next bullet.) The thread downloads CSV data that contains financial information about the stock and saves the data to a string variable. To download CSV data, the RealTimeHttpDownloader object’s static getRealTimeQuotes method is called. New stock information is placed in an ArrayList instance. To place the stock information into an ArrayList instance, the RealTimeHttpDownloader object’s static parseRealTimeData method is called. This method accepts a string variable that contains financial data and returns the ArrayList instance. Each element within the ArrayList instance is a Stock instance. New stock information is compared with existing stock information. That is, two ArrayList instances are compared. The new stock list and the current stock list. Only new stock information is placed into messages and sent to the destination. As a result, the client application is only getting messages that represent updated stock information. If information has not changed, then it is not placed within a message. To compare the two ArrayList instances, the compareStocks method is called. This method accepts two ArrayList instances and returns an ArrayList instance that represents the changes. Also the changes ArrayList instance is used to create the currentStock ArrayList instance that is used for the next call to compareStocks. For each instance in the changed ArrayList instance, a new AsyncMessage instance is created. Creating Live Stock Quote Sample Applications using the Message service The following code example represents the run method that performs these tasks. public void run() { ArrayList<Stock> currentStocks = new ArrayList<Stock>(); MessageBroker msgBroker = MessageBroker.getMessageBroker(null); String clientID = UUIDUtils.createUUID(); while (running) { //Check to see if a client application has //request stock symbols if (!RealTimeHttpDownloader.currentSymbolsIsEmpty()) { //Download a CSV data file and save to string String data = RealTimeHttpDownloader.getRealTimeQuotes(); //Parse data and put each stock in the newStocks ArrayList instance ArrayList<Stock> newStocks = RealTimeHttpDownloader.parseRealTimeData(data); ArrayList<Stock> changes = compareStocks(currentStocks, newStocks); currentStocks = (ArrayList<Stock>) newStocks.clone(); //Iterate thought the changed ArrayList for (int i = 0; i < changes.size(); i++) { AsyncMessage msg = new AsyncMessage(); msg.setDestination("stockFeed"); msg.setHeader("DSSubtopic", changes.get(i).getSymbol()); msg.setClientId(clientID); msg.setMessageId(UUIDUtils.createUUID()); msg.setTimestamp(System.currentTimeMillis()); msg.setBody(changes.get(i)); msgBroker.routeMessageToService(msg, null); } } } } Notice that for each element in the changed ArrayList instance, a new AsyncMessage instance is created. There is a 1-1 relationship between an AsyncMessage instance and a message that is sent. That is, the AsyncMessage instance is the message that is sent to the stockFeed destination. The AsyncMessage object’s setDestination method is called that defines the destination on the server to which the message is sent. The argument for the setDestination method is a Creating Live Stock Quote Sample Applications using the Message service string value that specifies the destination name. In this example, the stockFeed destination was configured by using XML files. (See Configure the destinations on the server.) Next, the AyncMessage object’s setHeader method is called. This method accepts a string value whose value is DSSubtopic. This value informs LiveCycle Data Services to use subtopics. That is, messages are sent to a subtopic of the stockFeed destination. In this example, the subtopic is the symbol name that corresponds to the stock. When the stockFeed destination was defined, the allow-subtopics attribute was set to true. The setClientID method sets the identifier value of the message. The identifier value is a universally unique value that was obtained by calling the UUIDUtils object’s static createUUID method. (This application logic is shown in the run method.) A timestamp is defined for the message by calling the AsyncMessage instance’s setTimeStamp method. The value of the System.currentTimeMillis is passed as an argument value. The message body is a Stock instance that is retrieved from the ArrayList instance that contains the changes. The index value is used to retrieve the correct Stock instance in the ArrayList instance. The MessageBroker object’s routeMessageToService method is called. This method sends the message to the stockFeed destination. StockQuoteFeed and StockQuoteFeedThread classes The following code example represents a Java file that contains both the StockQuoteFeed and StockQuoteFeedThread classes. Notice that these classes belong to a Java package named feed. package feed; import import import import import import httpDownloader.RealTimeHttpDownloader; java.util.ArrayList; flex.messaging.MessageBroker; flex.messaging.messages.AsyncMessage; flex.messaging.util.UUIDUtils; stock.Stock; public class StockQuoteFeed { private static StockQuoteFeedThread thread; public StockQuoteFeed() { } Creating Live Stock Quote Sample Applications using the Message service //If thread is null, allocate memory to thread and invoke its start memory public void start() { if (thread == null) { thread = new StockQuoteFeedThread(); thread.start(); } } public void stop() { thread.running = false; thread = null; } public static void main (String arg[]) { StockQuoteFeed feed = new StockQuoteFeed(); feed.start(); System.out.println("StockQuoteFeed Started"); RealTimeHttpDownloader.addStockQuote("ADBE"); } public static class StockQuoteFeedThread extends Thread { public boolean running = true; public static int test = 0; public static ArrayList<Stock> compareStocks (ArrayList<Stock> currentStocks, ArrayList<Stock> newStocks) { ArrayList<Stock> changes = new ArrayList<Stock>(); //Clone so we don't lose the values in newStock //(will be needed to update current changes = (ArrayList<Stock>) newStocks.clone(); //Remove all values that did not change //since last check (all those in newStocks that are also in currentStocks) changes.removeAll(currentStocks); return changes; } public void run() { ArrayList<Stock> currentStocks = new ArrayList<Stock>(); MessageBroker msgBroker = MessageBroker.getMessageBroker(null); String clientID = UUIDUtils.createUUID(); while (running) { //Check to see if a client application has request stock symbols if (!RealTimeHttpDownloader.currentSymbolsIsEmpty()) { Creating Live Stock Quote Sample Applications using the Message service //Download a CSV data file and save to string String data = RealTimeHttpDownloader.getRealTimeQuotes(); //Parse data and put each stock in the newStocks ArrayList instance ArrayList<Stock> newStocks = RealTimeHttpDownloader.parseRealTimeData(data); ArrayList<Stock> changes = compareStocks(currentStocks, newStocks); currentStocks = (ArrayList<Stock>) newStocks.clone(); //Iterate through the change for (int i = 0; i < changes.size(); i++) { AsyncMessage msg = new AsyncMessage(); msg.setDestination("stockFeed"); msg.setHeader("DSSubtopic", changes.get(i).getSymbol()); msg.setClientId(clientID); msg.setMessageId(UUIDUtils.createUUID()); msg.setTimestamp(System.currentTimeMillis()); msg.setBody(changes.get(i)); msgBroker.routeMessageToService(msg, null); } } } } } } Understanding the HttpDownloader class The HttpDownloader class represents the base class that is used to download information from Yahoo finance. This class contains a method named downloadFile that takes a string parameter named uri. The uri parameter represents the URL from which the data is downloaded. The downloadFile method returns a string value that represents the download response. The following Java code represents the downloadFile method. protected static String downloadFile(String uri) { HttpClient httpClient = new HttpClient(); HttpMethod getMethod = new GetMethod(uri); try { int response = httpClient.executeMethod(getMethod); if (response != 200) { throw new HttpException ("HTTP problem, httpcode: " + response); } InputStream inputStream = getMethod.getResponseBodyAsStream(); Creating Live Stock Quote Sample Applications using the Message service String responseText = streamToString(inputStream); return responseText; } catch (HttpException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; //returns null if error was thrown } The following Java code represents the entire HttpDownloader class. package httpDownloader; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import import import import org.apache.commons.httpclient.HttpClient; org.apache.commons.httpclient.HttpException; org.apache.commons.httpclient.HttpMethod; org.apache.commons.httpclient.methods.GetMethod; /** * Superclass containing only static methods used by *both <code>RealTimeHttpDownloader</code> * and <code>HistoryHttpDownloader</code>. * This class and its subclasses are based on the * YahooDownloader class located on wikiJava. * */ public class HttpDownloader { /** * Connects to the given URI using an <code>HttpClient</code> and a * <code>HttpMethod</code> object. Receives the downloaded information * in an <code>inputStream</code> and sends it to the * <code>streamToString()</code> method to convert it to a String. * * @param uri : The uri where the method will download the information * @return The downloaded response in String format */ protected static String downloadFile(String uri) { HttpClient httpClient = new HttpClient(); HttpMethod getMethod = new GetMethod(uri); Creating Live Stock Quote Sample Applications using the Message service try { int response = httpClient.executeMethod(getMethod); if (response != 200) { throw new HttpException ("HTTP problem, httpcode: " + response); } InputStream inputStream = getMethod.getResponseBodyAsStream(); String responseText = streamToString(inputStream); return responseText; } catch (HttpException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; //returns null if error was thrown } /** * Converts the given InputStream to a String using * the StringBuilder object. * * @param inputStream : The InputStream to be converted * @return A String representation of the given <code>InputStream</code> * @throws IOException Thrown by the * bufferedInputStream.read() method */ private static String streamToString(InputStream inputStream) throws IOException { BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); StringBuilder stringBuilder = new StringBuilder(); byte[] buffer = new byte[1024]; int bytesRead = 0; while ((bytesRead = bufferedInputStream.read(buffer)) != -1) { stringBuilder.append(new String(buffer, 0, bytesRead)); } return stringBuilder.toString(); } /** * Converts a String to a Double using the Double.parseDouble() Creating Live Stock Quote Sample Applications using the Message service * method. If the conversion fails, will simply return 0. * The conversion will * only fail when the String is equal to "N\A", as * this is the only non-numerical * value the downloaded file may have in a field where a number is expected. * * @param number : The String representation of the number to convert * @return The given number as a Double. Returns 0 if the String cannot be * converted to a Double. */ protected static Double parseDouble(String number) { number = number.replace("\"", ""); try { return Double.parseDouble(number); } catch (NumberFormatException e) { return 0.0; } } } Understanding the RealTimeHttpDownloader class The RealTimeHttpDownloader class extends the HttpDownloader class and downloads realtime stock information from Yahoo finance. This data is used to populate the data grid control located in the client application. This class creates ArrayList instances that are used to create messages that are sent to the stockFeed destination. All methods located in this class are static. Within the StockQuoteFeedThread object’s run method, the RealTimeHttpDownloader object’s static getRealTimeQuotes method is called. The getRealTimeQuotes method calls the HttpDownloader object’s downloadFile method. Remember that the downloadFile method takes a uri parameter value that specifies the URL from which to download information. The downloadFile method returns CSV data that represents stock information within a string value. The following Java code example shows the getRealTimeQuotes method. public static String getRealTimeQuotes() { return downloadFile(getRealTimeURI()); } Creating Live Stock Quote Sample Applications using the Message service The uri argument is obtained by calling the getRealTimeURI method. The getRealTimeURI method constructs the URL that is required to retrieve CSV data from Yahoo finance. The following Java code example represents the getRealTimeURI method. private static String getRealTimeURI() { String uri = "http://finance.yahoo.com/d/quotes.csv?s="; for (int i = 0; i < currentSymbols.size(); i++) { uri += currentSymbols.get(i); if (i+1 < currentSymbols.size()) { uri += "+"; } } return uri += "&f=snl1c6ohgd1t1"; } In this example, currentSymbols is an ArrayList data member that belongs to the RealTimeHttpDownloader class. The currentSymbols data member tracks all of the symbols that a client application tracks. Therefore each symbol that is tracked by a client application is specified within the URL that is sent to the Yahoo content provider. Stock symbols that are tracked are added to the currentSymbols data member by calling the static addStockQuote method. This method accepts a string value that represents the stock symbol. If the input argument is not a valid stock symbol, then an exception is thrown when the CSV data is parsed and this method returns null. Note: The addStockQuote method is called by an ActionScript object defined in the client application that references the setQuotes destination. (See Develop the client application logic.) The following Java code represents the addStockQuote method. public static Stock addStockQuote (String symbol) { String initData = downloadFile("http://finance.yahoo.com/d/quotes.csv?s=" + symbol + "&f=snl1c6ohgd1t1"); try { Stock aStock = (parseRealTimeData(initData)).get(0); if (!currentSymbols.contains(symbol)) { currentSymbols.add(symbol); } System.out.println(currentSymbols.toString()); return aStock; } catch (Exception e) { Creating Live Stock Quote Sample Applications using the Message service return null; } } Another important task that the RealTimeHttpDownloader class performs is to create an ArrayList object where each element is a Stock instance. The run method located in the StockQuoteFeedThread class calls the static parseRealTimeData method. This method accepts a string variable that represents the CSV data returned by the content provider. This method constructs the ArrayList object where each element is a Stock instance. The following Java code represents the parseRealTimeData method. public static ArrayList<Stock> parseRealTimeData (String data) throws ParseException { ArrayList<Stock> newStocks = new ArrayList<Stock>(); String[] csvRows = data.split("\n"); for (int i = 0; i < csvRows.length; i++) { String[] stockInfo = csvRows[i].split(","); Stock aStock = new Stock(stockInfo[0].replace("\"", ""), stockInfo[1].replace("\"", ""), parseDouble(stockInfo[2]), parseDouble(stockInfo[3]), parseDouble(stockInfo[4]), parseDouble(stockInfo[5]), parseDouble(stockInfo[6]), convertToDate(stockInfo[7], stockInfo[8])); newStocks.add(aStock); } return newStocks; } The first task that this method performs is to create an ArrayList object that contains stock information. This method splits data located in the string input parameter that contains CSV data into rows by using a new line character (\n). Each row represents a different stock and is stored as an element in a string array named csvRows. For each row (or element in the csvRows array), a new string array named stockInfo is declared by splitting the data using a comma character. A new Stock instance is created and its data members are populated with data located the stockInfo string array. The Stock instance is added to the ArrayList object. The parseRealTimeData method returns the ArrayList object that contains a Stock object for each stock located in the CSV data. Creating Live Stock Quote Sample Applications using the Message service RealTimeHttpDownloader class The following Java code represents the entire RealTimeHttpDownloader class. Notice that this class belongs to a package named httpDownloader package httpDownloader; import import import import import import java.text.DateFormat; java.text.ParseException; java.text.SimpleDateFormat; java.util.ArrayList; java.util.Date; stock.Stock; public class RealTimeHttpDownloader extends HttpDownloader { /** Static ArrayList containing the symbols that are being queried * by the StockQuoteFeed */ private static ArrayList<String> currentSymbols = new ArrayList<String>(); public static boolean currentSymbolsIsEmpty() { return currentSymbols.isEmpty(); } public static String getRealTimeQuotes() { return downloadFile(getRealTimeURI()); } public static String addStockQuote (String symbol) { if (!currentSymbols.contains(symbol)) { currentSymbols.add(symbol); } System.out.println(currentSymbols.toString()); return symbol; } public static void removeStockQuote (String symbol) { currentSymbols.remove(symbol); } private static String getRealTimeURI() { String uri = "http://finance.yahoo.com/d/quotes.csv?s="; for (int i = 0; i < currentSymbols.size(); i++) { uri += currentSymbols.get(i); if (i+1 < currentSymbols.size()) { Creating Live Stock Quote Sample Applications using the Message service uri += "+"; } } return uri += "&f=snl1c6ohgd1t1"; } //This method parses the CSV data, creates a Stock instance and //places the Stock instance in an ArrayList public static ArrayList<Stock> parseRealTimeData (String data) { ArrayList<Stock> newStocks = new ArrayList<Stock>(); String[] csvRows = data.split("\n"); for (int i = 0; i < csvRows.length; i++) { String[] stockInfo = csvRows[i].split(","); Stock aStock = new Stock(stockInfo[0].replace("\"", ""), stockInfo[1].replace("\"", ""), parseDouble(stockInfo[2]), parseDouble(stockInfo[3]), parseDouble(stockInfo[4]), parseDouble(stockInfo[5]), parseDouble(stockInfo[6]), convertToDate(stockInfo[7], stockInfo[8])); newStocks.add(aStock); } return newStocks; } private static Date convertToDate (String sDate, String sTime) { sDate = sDate.replace("\"", ""); sDate = sDate.replace("\r", ""); sTime = sTime.replace("\"", ""); sTime = sTime.replace("\r", ""); sDate += " " + sTime; try { DateFormat dateformater = new SimpleDateFormat("M/d/yyyy KK:mmaa"); return dateformater.parse(sDate); } catch (ParseException e) { e.printStackTrace(); } return null; } } Creating Live Stock Quote Sample Applications using the Message service Understanding the HistoryHttpDownloader class The HistoryHttpDownloader class extends the HttpDownloader class and downloads history stock information from Yahoo finance. This data populates the area chart and line chart controls located in the client application. For example, if the client application requests to view data that represents stock activity during the past five years, this class creates the data. The StockQuoteFeedThread class does not call methods that belong to the HistoryHttpDownloader class. Instead, the client application calls methods exposed by this class by using Remoting functionality supported by LiveCycle Data Services. This class contains a method named getHistoricalQuotes that accepts three parameter values: A string value that specifies the stock symbol for which historical data is retrieved. A date value that specifies the start dare. A date value that specifies the end date. The getHistoricalQuotes method is called by an ActionScript object that is defined in the client application that references the getStockHistory destination. The following Java code represents the getHistoricalQuotes method. public static ArrayList<StockInTime> getHistoricalQuotes(String symbol, Date from, Date to) { String data = downloadFile(getHistoryURI(symbol, from, to)); ArrayList<StockInTime> stockHistory = parseHistoryData(data); return stockHistory; } The first task that this method performs is to call the downloadFile method located in the HttpDownloader class. The URL value that is passed to the downloadFile method is created by calling the getHistoryURI method. This method requires the input parameter values that are passed to the HistoricalQuotes method. The HistoricalQuotes method constructs a URL value that is passed to Yahoo finance. This URL value includes date information along with the stock symbol. The following Java code represents the getHistoryURI method. Creating Live Stock Quote Sample Applications using the Message service private static String getHistoryURI (String symbol, Date from, Date to) { Calendar fromDate = new GregorianCalendar(); fromDate.setTime(from); Calendar toDate = new GregorianCalendar(); toDate.setTime(to); String uri = "http://ichart.finance.yahoo.com/table.csv?s="; uri += symbol; uri += "&a=" + fromDate.get(Calendar.MONTH); uri += "&b=" + fromDate.get(Calendar.DAY_OF_MONTH); uri += "&c=" + fromDate.get(Calendar.YEAR); uri += "&d=" + toDate.get(Calendar.MONTH); uri += "&e=" + toDate.get(Calendar.DAY_OF_MONTH); uri += "&f=" + toDate.get(Calendar.YEAR); return uri += "&g=d"; } The return value of getHistoryURI is passed to the downloadFile method in the HttpDownloader class. The return value of the downloadFile method is CSV data returned by Yahoo finance. This data represents the historical data that is displayed in the area chart control located in the client application. The CSV data returned by the downloadFile method is passed to the parseHistoryData method. This method constructs the ArrayList object where each element is a StockInTime instance. A StockInTime instance represents a save point of data in a stock's history. The date and close data members are used as (x,y) coordinates that are plotted in the line chart control located in the client application. The following Java code represents the parseHistoryData method. public static ArrayList<StockInTime> parseHistoryData (String data) { ArrayList<StockInTime> stockHistory = new ArrayList<StockInTime>(); String[] csvRows = data.split("\n"); //First row contains headers, ignored for (int i = 1; i < csvRows.length; i++) { String[] stockInfo = csvRows[i].split(","); StockInTime stockPoint = new StockInTime(convertToDate(stockInfo[0]), parseDouble(stockInfo[4])); stockHistory.add(stockPoint); } return stockHistory; } Creating Live Stock Quote Sample Applications using the Message service The first task that this method performs is to create an ArrayList object named parseHistoryData that contains stock information. This method splits data located in the CSV input parameter into rows by using a new line character (\n). Each row represents a different stock and is stored as an element in a string array named csvRows. For each row (or element in the csvRows array), a new string array named stockInfo is created by splitting the data by using a comma character. A new StockInTime instance is created and its data members are populated with data located the stockInfo string array. The StockInTime instance is added to the parseHistoryData object. The parseHistoryData method returns the ArrayList object that contains a StockInTime object for each stock located in the CSV data. The return value of the getHistoricalQuotes method is the ArrayList object that contains a StockInTime object for each stock located in the CSV data. HistoryHttpDownloader class The following Java code represents the entire HistoryHttpDownloader class. Notice that this class belongs to a package named httpDownloader. package httpDownloader; import import import import import import import java.text.DateFormat; java.text.ParseException; java.text.SimpleDateFormat; java.util.ArrayList; java.util.Calendar; java.util.Date; java.util.GregorianCalendar; import stock.StockInTime; public class HistoryHttpDownloader extends HttpDownloader { public static ArrayList<StockInTime> getHistoricalQuotes(String symbol, Date from, Date to) { String data = downloadFile(getHistoryURI(symbol, from, to)); ArrayList<StockInTime> stockHistory = parseHistoryData(data); return stockHistory; } private static String getHistoryURI (String symbol, Date from, Date to) { Calendar fromDate = new GregorianCalendar(); Creating Live Stock Quote Sample Applications using the Message service fromDate.setTime(from); Calendar toDate = new GregorianCalendar(); toDate.setTime(to); String uri = "http://ichart.finance.yahoo.com/table.csv?s="; uri += symbol; uri += "&a=" + fromDate.get(Calendar.MONTH); uri += "&b=" + fromDate.get(Calendar.DAY_OF_MONTH); uri += "&c=" + fromDate.get(Calendar.YEAR); uri += "&d=" + toDate.get(Calendar.MONTH); uri += "&e=" + toDate.get(Calendar.DAY_OF_MONTH); uri += "&f=" + toDate.get(Calendar.YEAR); return uri += "&g=d"; } public static ArrayList<StockInTime> parseHistoryData (String data) { ArrayList<StockInTime> stockHistory = new ArrayList<StockInTime>(); String[] csvRows = data.split("\n"); //First row contains headers, ignored for (int i = 1; i < csvRows.length; i++) { String[] stockInfo = csvRows[i].split(","); StockInTime stockPoint = new StockInTime(convertToDate(stockInfo[0]), parseDouble(stockInfo[4])); stockHistory.add(stockPoint); } return stockHistory; } private static Date convertToDate (String sDate) { try { DateFormat dateformater = new SimpleDateFormat("yyyy-MM-dd"); return dateformater.parse(sDate); } catch (ParseException e) { e.printStackTrace(); } return null; } } Understanding the Stock class The Stock class contains data members that store stock information such as high value, low value, open value, and so on. The following Java code represents the Stock class. Notice that this class belongs to a package named stock. Creating Live Stock Quote Sample Applications using the Message service package stock; import java.io.Serializable; import java.util.Date; /** * This POJO stores detailed information concerning a stock * at a particular time. * */ public class Stock implements Serializable { private private private private private private private private private static final long serialVersionUID = -8334804402463267285L; String symbol; String name; double low; double high; double open; double last; double change; Date tradeTime; public Stock (String symbol, String name, double last, double change, double open, double high, double low, Date tradeTime) { this.symbol = symbol; this.last = last; this.name = name; this.low = low; this.high = high; this.open = open; this.change = change; this.tradeTime = tradeTime; } public double getChange() { return change; } public void setChange(double change) { this.change = change; } public double getHigh() { return high; Creating Live Stock Quote Sample Applications using the Message service } public void setHigh(double high) { this.high = high; } public double getLast() { return last; } public void setLast(double last) { this.last = last; } public double getLow() { return low; } public void setLow(double low) { this.low = low; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getOpen() { return open; } public void setOpen(double open) { this.open = open; } public String getSymbol() { return symbol; } public void setSymbol(String symbol) { this.symbol = symbol; } Creating Live Stock Quote Sample Applications using the Message service public Date getTradeTime() { return tradeTime; } public void setTradeTime(Date tradeTime) { this.tradeTime = tradeTime; } @Override public boolean equals(Object aStock) { //Check self comparison if ( this == aStock ) return true; //Check instance if ( !(aStock instanceof Stock) ) return false;; //Cast to object is safe due to above check Stock stock = (Stock) aStock; //Field-by-field comparison return this.symbol.equals(stock.symbol) && this.last == stock.last && this.name.equals(stock.name) && this.low == stock.low && this.high == stock.high && this.open == stock.open && this.change == stock.change; } @Override public String toString() { return this.name + " (" + this.symbol + "): " + this.last + (this.change>0? " +" + this.change : " " + this.change); } } Understanding the StockInTime class The StockInTime class represents a save point of data in a stock's history. The following Java code represents the StockInTime class. Notice that this class belongs to a package named stock. package stock; Creating Live Stock Quote Sample Applications using the Message service import java.util.Date; /** * This POJO is used to save each point of data in a stock's history. The * attributes "date" and "close" are used as (x,y) coordinates that can be * plotted on a graph. * */ public class StockInTime { /** The date value represent the day associated * with a particular closing value of a stock. */ public Date date; /** The close values represent the closing value of a * stock on a particular day. */ public Double close; /** Simple constructor that associates given parameters to attributes */ public StockInTime(Date date, Double close) { this.date = date; this.close = close; } } Deploy the stock quote application server classes After you create and compile the Java server-based classes, you deploy them to the server hosting LiveCycle Data Services. The compiled Java CLASS files are deployed to the WEBINF\classes folder located in your web application. For example, assume that the name of your web application is stockDashboard. In this situation, deploy the CLASS files in the following directory. [Install directory]lcds\tomcat\webapps\stockDashboard\WEB-INF\classes where [Install directory] is the directory on which LiveCycle Data Services is installed. Under the classes folder, create a folder structure that matches the package names. The StockQuoteFeed and StockQuoteFeedThread classes both belong to the feed package. As a result, create the following folder structure: WEB-INF\classes\feed Place the StockQuoteFeed.class and StockQuoteFeedThread.class files in this folder. Creating Live Stock Quote Sample Applications using the Message service Because the HistoryHttpDownloader, HttpDownloader, RealTimeHttpDownloader classes belong to the httpDownloader package, create the following folder structure: WEB-INF\classes\httpDownloader Place these CLASS files in this folder. Lastly, create the following folder structure: WEB-INF\classes\stock Place the Stock.class and StockInTime.class files in this folder Ensure that the LiveCycle Data Service JAR files are located in the WEB-INF\lib folder. Create the client application using Flash Builder Create a Flash Builder project that is used to create the client application. This project references the J2EE application server hosting LiveCycle Data Services. That is, when you create the project, select J2EE as the Application Server type and LiveCycle Data Services as the application server. After you create the project, all of the client libraries required to interact with the J2EE application server are added to your project’s class path. To create a client project by using Flash Builder 4, perform the following steps: 1. Start Flash Builder 4 by clicking Start, All Programs, Adobe Flash Builder 4. 2. Create a new project. 3. In the Project Name box, specify a name for your project. 4. Under Application Type, select Web. 5. Specify version 4.0 for the Flex SDK version. 6. In the Application Server list, select J2EE. 7. Select the Use Remote Access Service check box. 8. Select LiveCycle Data Services check box. Creating Live Stock Quote Sample Applications using the Message service 9. In the Root folder box, specify the root folder value. For example, specify C:\lcds\tomcat\webapps\stockDashboard. 10. In the Root URL box, specify the root URL folder value. For example, specify http://localhost:8400/stockDashboard/. 11. In the Content root box, specify the Context root value. For example, specify /stockDashboard. 12. Accept the Output folder default value. 13. Click Finish. Develop the client application logic After you create a project for the client application, add the required files to the project. The stock quote application consists of the following files: financeDashboard.mxml - represents the main application file. This file defines the Consumer objects that subscribe to the feedStock destination. historyChart.mxml - displays the finance data within an AreaChart control. liveChart.mxml - displays the finance data within a LineChart control. DateRange.as - represents a date range that is used to set slider controls. Stock.as - represents the client implementation of the Stock server-side Java class. StockInTime.as - represents the client implementation of the StockInTime serverside Java class. The following illustration shows the Flash Builder project that creates the stock quote application. Creating Live Stock Quote Sample Applications using the Message service Note: The SWC files are automatically added to the project’s class path when you create a new project as described in the Create a Flash Builder project topic. Create the financeDashboard file The financeDashboard.mxml file is the main application file for the stock quote application. This client application displays message information (stock information) within a DataGrid control. This control consists of the following columns: Name - displays the name of the company Symbol - displays the stock symbol Last - displays the stock’s last price Change - displays the change to the stock price Open - displays the stock’s opening price Day High - displays the stock’s high price Day Low - displays the stocks low price Creating Live Stock Quote Sample Applications using the Message service The DataGrid control, named stockGrid, is populated by an ArrayCollection instance named gridData. This object is a public data member that belongs to the financeDashboard.mxml file. Because this object is used to populate the data grid control, it is declared as Bindable, as shown in the following example. [Bindable] public var gridData:ArrayCollection = new ArrayCollection(); Setting up a multi topic consumer The client application receives messages from different subtopics that are located in the stockFeed destination. As a result, a MultiTopicConsumer object named consumer is defined. The following code example shows the MultiTopicConsumer object being created. <mx:MultiTopicConsumer id="consumer" destination="stockFeed" message="handleMessage(event)" /> This object type enables you to register subscriptions from a list of subtopics within a destination. A MultiTopicConsumer dispatches a MessageEvent for each message it receives. That is, each time a message is received, an event is dispatched to handleMessage as defined by the MultiTopicConsumer object’s message property. Adding new symbols by using a RemoteObject A RemoteObject instance named roStockModifier is defined that interacts with the setQuotes destination, as shown in the following code example. <s:RemoteObject id="roStockModifier" destination="setQuotes" result="resultAddStockQuote(event)"/> The roStockModifier object is responsible for adding symbols to the thread running on the server. No messages are returned back to the client application until a symbol is set by calling this method. Symbols are added to the thread running on the server as a result of a user entering the name of the symbol into the TextInput control named inputSymbol. When the user clicks the Add Symbol button, a call to the addSymbol method is made. The following ActionScript code represents the addSymbol method. protected function addSymbol(event:MouseEvent):void { roStockModifier.addStockQuote(inputSymbol.text); consumer.addSubscription(inputSymbol.text); cursorManager.setBusyCursor(); } Creating Live Stock Quote Sample Applications using the Message service The roStockModifier remote object calls the method named addStockQuote. This method is defined in the RealTimeHttpDownloader Java server-side class. This method accepts a string argument that represents the symbol to add to the server thread. (See Understanding the RealTimeHttpDownloader class.) Also notice that the MultiTopicConsumer object’s addSubscription method is called. This method results in the client application monitoring the subtopic within the destination. The name of the subtopic corresponds to the symbol value. For example, assume that a user enters ADBE into the inputSymbol text input control and clicks the Add Symbol button. This user action results in the followng two actions: The server thread creates messages that contain financial information for Adobe. The server thread sends the messages to the ADBE subtopic within the stockFeed destination. The client application is set to monitor the ADBE subtopic within the stockFeed destination for new messages. Handling incoming messages Now that a symbol is added to the server thread, the server starts creating messages and sending them to the stockFeed destination. When the client application detects new messages that are sent to the stockFeed destination, the handleMessage method is called. Messages that are retrieved from the stockFeed destination are Stock instances. A Stock instance is defined on the client as instances of the Stock class. The following ActionScript code represents the handleMessage method. private function handleMessage(event:MessageEvent):void { var newStock:Stock = event.message.body as Stock; var index:int = findStock(newStock.symbol); if (index >= 0) { //Stock already exists var targetStock:Stock = gridData.getItemAt(index) as Stock; if (targetStock.tradeTime.minutes < newStock.tradeTime.minutes) { targetStock.daily.addItem({Time:newStock.tradeTime, Last:newStock.last}); } targetStock.last = newStock.last; targetStock.change = newStock.change; Creating Live Stock Quote Sample Applications using the Message service targetStock.open = newStock.open; targetStock.high = newStock.high; targetStock.low = newStock.low; targetStock.tradeTime = newStock.tradeTime; else { //Stock is new newStock.daily.addItem({Time:newStock.tradeTime, Last:newStock.last}); gridData.addItem(newStock); } gridData.refresh(); } In the handleMessage method, a new Stock instance named newStock is created by getting the value of event.message.body. This value is cast to a Stock instance. The findStock method is called that returns the index value of the stock with the indicated symbol in the gridData array collection. If the symbol exists, then a new Stock instance named targetStock is created that represents the existing Stock in the array collection. If the stock symbol does not exist, then the findStock method returns -1. The properties of targetStock are modified with the properties of new newStock. As a result, the changes represented by newStock are displayed in the data grid control. If the stock symbol does not exist in the array collection, then newStock is added to the array collection. The data grid control displays the new information represented by newStock. financeDashboard file The following code represents the entire financeDashboard.mxml file. <?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" creationComplete="init(event)" height="1058" width="1544" xmlns:Components="Components.*"> <fx:Style source="financeDashboard.css"/> <fx:Declarations> <!-- Place non-visual elements (e.g., services, value objects) here --> <mx:MultiTopicConsumer id="consumer" destination="stockFeed" message="handleMessage(event)" /> <s:RemoteObject id="roStockModifier" destination="setQuotes" result="resultAddStockQuote(event)"/> Creating Live Stock Quote Sample Applications using the Message service <s:Sequence id="newpanel" target="{chartPanel}"> <s:Fade id="movein" duration="500" alphaFrom="0" alphaTo="1.0"/> </s:Sequence> <fx:Date id="today"/> </fx:Declarations> <fx:Script> <![CDATA[ import import import import import import import import import import import import import mx.collections.ArrayCollection; mx.collections.ListCollectionView; mx.controls.Alert; mx.events.FlexEvent; mx.messaging.ChannelSet; mx.messaging.Consumer; mx.messaging.MultiTopicConsumer; mx.messaging.config.ServerConfig; mx.messaging.events.MessageEvent; mx.messaging.messages.IMessage; mx.rpc.events.ResultEvent; stock.DateRange; stock.Stock; /** Data Provider for the dataGrid */ [Bindable] public var gridData:ArrayCollection = new ArrayCollection(); /** Stock currently displayed in the chartPanel */ [Bindable] public var detailStock:Stock; private function init(event:FlexEvent):void { chartPanel.visible = false; consumer.subscribe(); } /** * When a message is received form the server with new stock data, * this funciton will either add it to the data provider (if new) * of it will update the value in the data provider (if it is * already there) */ private function handleMessage(event:MessageEvent):void { var newStock:Stock = event.message.body as Stock; var index:int = findStock(newStock.symbol); Creating Live Stock Quote Sample Applications using the Message service if (index >= 0) { //Stock already exists var targetStock:Stock = gridData.getItemAt(index) as Stock; //Only add the data for the live chart every minute if (targetStock.tradeTime.minutes < newStock.tradeTime.minutes) { targetStock.daily.addItem({Time:newStock.tradeTime, Last:newStock.last}); } //Set the new values of the received stock targetStock.last = newStock.last; targetStock.change = newStock.change; targetStock.open = newStock.open; targetStock.high = newStock.high; targetStock.low = newStock.low; targetStock.tradeTime = newStock.tradeTime; } else { //Stock is new //Add stock to the datProvider newStock.daily.addItem({Time:newStock.tradeTime, Last:newStock.last}); gridData.addItem(newStock); } gridData.refresh(); } /** Returns the index of the stock with the indicated symbol * in the gridData array collection. If it is not in this * array collection, the value -1 is returned * * @param symbol: The symbol of the stock * @return Index of this stock in stockValues. -1 if not found */ private function findStock(symbol:String):int { for (var i:int = 0; i < gridData.length; i++) { if (gridData.getItemAt(i).symbol == symbol) { return i; } } return -1; } /** * Remote method call to add subscribe to a new symbol. The server will * return intial data that will be added to the dataGrid, or will return * null to indicate this symbol is not supported. Creating Live Stock Quote Sample Applications using the Message service * * The remote object method call is handled in the function * "resultAddStockQuote" */ protected function addSymbol(event:MouseEvent):void { roStockModifier.addStockQuote(inputSymbol.text); consumer.addSubscription(inputSymbol.text); cursorManager.setBusyCursor(); } /** * Handles data returned by the server after attemtping to add a new * stock symbol. If null is received than the stock symbol is not * available. If the symbol is already in the data provider than it * is ignored and the proper message is displayed. If the symbol was * valid than it is added to the dataprovider. */ private function resultAddStockQuote(event:ResultEvent):void { if (event.result != null) { var newStock:Stock = event.result as Stock; if (findStock(newStock.symbol) < 0) { newStock.daily.addItem({Time:newStock.tradeTime, Last:newStock.last}); gridData.addItem(newStock); gridData.refresh(); } else { Alert.show("This stock symbol has already been added!", "Symbol already exists",4,this); } } else { Alert.show("This stock symbol is not available.", "Wrong Stock Symbol",4,this); } cursorManager.removeBusyCursor(); } /** * This mehtod simply unsubscribes the user from the given * stock symbol and removes the entry form the data provider. * This does not make any changes on the server side. As an * exercise, the reader could extend this application to * dynamically change the queried symbols to remove symbol * that no client is currently viewing. */ protected function removeSymbol(event:MouseEvent):void { consumer.removeSubscription(stockGrid.selectedItem.symbol); Creating Live Stock Quote Sample Applications using the Message service var index:int = findStock(stockGrid.selectedItem.symbol); gridData.removeItemAt(index); chartPanel.visible = false; } /** * Displays the charPanel containing the detailed information * of the selected stock. */ protected function displayDetails(event:Event):void { removeBtn.enabled = true; chartPanel.visible = true; newpanel.play(); detailStock = stockGrid.selectedItem as Stock; historyChart.getStockHistory(new DateRange()); } ]]> </fx:Script> <!-- DataGrid Panel --> <s:Panel x="6" y="10" width="901" height="249"> <mx:DataGrid id="stockGrid" x="9" y="10" width="684" height="196" fontSize="10" dataProvider="{gridData}" itemClick="displayDetails(event)"> <mx:columns> <mx:DataGridColumn dataField="name" headerText="Name"/> <mx:DataGridColumn dataField="symbol" headerText="Symbol"/> <mx:DataGridColumn dataField="last" headerText="Last"/> <mx:DataGridColumn dataField="change" headerText="Change"/> <mx:DataGridColumn dataField="open" headerText="Open"/> <mx:DataGridColumn dataField="high" headerText="Day High"/> <mx:DataGridColumn dataField="low" headerText="Day Low"/> </mx:columns> </mx:DataGrid> <s:Label id="asd" x="11" y="-22" text="Financial Dashboard" fontFamily="Verdana" fontSize="20" fontWeight="normal" fontStyle="normal" textDecoration="none"/> <s:Button x="701" y="52" label="Add Symbol" height="20" width="109" click="addSymbol(event)"/> <s:TextInput x="818" y="52" width="76" id="inputSymbol"/> <s:Button id="removeBtn" x="701" y="81" label="Remove Symbol" enabled="false" width="110" click="removeSymbol(event)"/> </s:Panel> Creating Live Stock Quote Sample Applications using the Message service <!-- Chart Panel --> <s:Panel id="chartPanel" x="6" y="267" width="901" height="602"> <s:Label id="dstockSymbol" text="{detailStock.symbol}" x="12" y="-21" fontFamily="Verdana" fontSize="20"/> <s:Label id="dstockLast" text="{String(detailStock.last)} ( {String(detailStock.change)} )" x="118" y="-21" fontFamily="Verdana" fontSize="20"/> <Components:liveChart id="liveChart" x="15" y="10" stockData="{detailStock.daily}" low="{detailStock.low}" high="{detailStock.high}" symbol="{detailStock.symbol}"/> <Components:historyChart id="historyChart" x="13" y="288" symbol="{detailStock.symbol}"/> </s:Panel> </s:Application> Create the historyChart file The historyChart.mxml file displays the finance data within an AreaChart control. The name of the AreaChart instance is stockHistoryChart, as shown in the following code snippet. <!-- stock history chart --> <mx:AreaChart id="stockHistoryChart" x="0" y="0" width="883" height="223" dataProvider="{stockHistory}" showDataTips="true"> <!-- seriesFilters="[]"--> <!-- horizontal axis --> <mx:horizontalAxis> <mx:DateTimeAxis id="dtAxis" dataUnits="days" displayLocalTime="true" /> </mx:horizontalAxis> <!-- vertical axis --> <mx:verticalAxis> <mx:LinearAxis id="lAxis"/> <!--minimum="{low}" maximum="{high}"--> </mx:verticalAxis> Creating Live Stock Quote Sample Applications using the Message service <!-- series --> <mx:series> <mx:AreaSeries displayName="{symbol}" xField="date" yField="close" alpha="0.8"/> </mx:series> </mx:AreaChart> As defined in this tag, the data provider for the AreaChart control is an ArrayCollection instance named stockHistory. The stockHistory instance is a public data member, as shown in the following code example. [Bindable] public var stockHistory:ArrayCollection = new ArrayCollection; The historyChart.mxml file defines a RemoteObject named roStockHistory. The destination for this remote object is getStockHistory. The result handler is a method named resultStockHistory, as shown in the following example. <s:RemoteObject id="roStockHistory" destination="getStockHistory" result="resultStockHistory(event)"/> The roStockHistory instance is used within a method named getStockHistory. The roStockHistory instance calls the getHistoricalQuotes method, as shown in the following code example. public function getStockHistory(range:DateRange):void { showLoading(); roStockHistory.getHistoricalQuotes(symbol, range.getFromDate(), range.getToDate()); } The getHistoricalQuotes method is defined in the HistoryHttpDownloader Java class. (See Understanding the HistoryHttpDownloader class.) This method accepts three parameter values: A string value that specifies the stock symbol for which historical data is retrieved. A date value that specifies the start date. A date value that specifies the end date. The resultStockHistory method is called when the getHistoricalQuotes method returns data. The getHistoricalQuotes method returns an ArrayCollection where each element Creating Live Stock Quote Sample Applications using the Message service is a StockInTime instance. The following ActionScript code example represents the resultStockHistory method. private function resultStockHistory(event:ResultEvent):void { stockHistory = event.result as ArrayCollection; timeSlider.values = [currentDateRange.getFromDate().getTime(), currentDateRange.getToDate().getTime()]; timeSlider.minimum = stockHistory.getItemAt(stockHistory.length1).date.getTime(); //get date of first available stock quote timeSlider.maximum = currentDateRange.getToDate().getTime(); dtAxis.minimum = new Date(timeSlider.minimum); dtAxis.maximum = new Date(timeSlider.maximum); sortArrayCollection(stockHistory, "close"); var axisBounds:Array = getMinMax(); lAxis.maximum = axisBounds[1]; lAxis.minimum = axisBounds[0]; stockHistory.refresh(); removeLoading(); } A stockHistory instance is created by getting event.result and casting the value to ArrayCollection. The stockHistory ArrayCollection is used to populate the AreaChart control. The following code represents the entire historyChart.mxml file. This file is placed within a package named Components. <?xml version="1.0" encoding="utf-8"?> <s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" width="883" height="281"> <fx:Declarations> <s:RemoteObject id="roStockHistory" destination="getStockHistory" result="resultStockHistory(event)"/> <s:BlurFilter id="blurFilter" quality="4"/> </fx:Declarations> <fx:Script> <![CDATA[ Creating Live Stock Quote Sample Applications using the Message service import import import import import import mx.collections.ArrayCollection; mx.collections.Sort; mx.collections.SortField; mx.rpc.events.ResultEvent; stock.DateRange; stock.StockInTime; /** dataProvider for the chart */ [Bindable] public var stockHistory:ArrayCollection = new ArrayCollection; /** Symbol of the stock currently displayed */ [Bindable] public var symbol:String; /** Current data range selected */ Bindable] public var currentDateRange:DateRange = new DateRange(); /** Represents one day in seconds. Used to set the steps of the slider control */ [Bindable] public var dailyStep:uint = 1000*60*60*24; /** * Remote prodecure call to obtain historical stock data. Handled in the * "resultStockHistory" function */ public function getStockHistory(range:DateRange):void { showLoading(); roStockHistory.getHistoricalQuotes(symbol, range.getFromDate(), range.getToDate()); } /** Sets the data provider of the chart and initializes the slider */ private function resultStockHistory(event:ResultEvent):void { stockHistory = event.result as ArrayCollection; timeSlider.values = [currentDateRange.getFromDate().getTime(), currentDateRange.getToDate().getTime()]; //set bounds timeSlider.minimum = stockHistory.getItemAt(stockHistory.length1).date.getTime(); //get date of first available stock quote timeSlider.maximum = currentDateRange.getToDate().getTime(); dtAxis.minimum = new Date(timeSlider.minimum); dtAxis.maximum = new Date(timeSlider.maximum); sortArrayCollection(stockHistory, "close"); //sort for faster axis resizing var axisBounds:Array = getMinMax(); //quicker due to sort lAxis.maximum = axisBounds[1]; lAxis.minimum = axisBounds[0]; stockHistory.refresh(); Creating Live Stock Quote Sample Applications using the Message service removeLoading(); } /** Sorts given arrayCollection by the given field in increasing numerical order*/ private function sortArrayCollection(aColl:ArrayCollection, field:String):ArrayCollection { var sField:SortField = new SortField(); sField.name = field; sField.numeric = true; var numSort:Sort = new Sort(); numSort.fields = [sField]; aColl.sort = numSort; aColl.refresh(); return aColl; } /** Change slider bounds and displayed information based on the toggle button pressed */ private function toggleButtonClick(event:MouseEvent):void { toggleButtons(event.target as ToggleButton); switch(event.target.id) { case "tb1M": { getStockHistory(currentDateRange.set1M()); break; case "tb3M": { getStockHistory(currentDateRange.set3M()); break; } case "tb6M": { getStockHistory(currentDateRange.set6M()); break; } case "tbYTD": { getStockHistory(currentDateRange.setYTD()); break; } case "tb1Y": { getStockHistory(currentDateRange.set1Y()); break; } Creating Live Stock Quote Sample Applications using the Message service case "tb5Y": { getStockHistory(currentDateRange.set5Y()); break; } case "tbAll": { getStockHistory(currentDateRange.setAll()); break; } } } /** Intialize slider thumb positions */ private function timeSlider_creationComplete(event:Event):void { timeSlider.values=[0,(new Date()).getTime()]; } /** Change chart according to slider values */ private function sliderRangeChange(event:Event):void { if (event.target.values[0] != dtAxis.minimum.getTime() || event.target.values[1] != dtAxis.maximum.getTime()) { sortArrayCollection(stockHistory, "close"); //Sort array by closing value var axisBounds:Array = getMinMax(); //This step is quicker due to the sort lAxis.maximum = axisBounds[1]; lAxis.minimum = axisBounds[0]; dtAxis.minimum = new Date(event.currentTarget.values[0]); dtAxis.maximum = new Date(event.currentTarget.values[1]); } } /** Ensures only the last clicked button is selected */ private function toggleButtons(tButton:ToggleButton):void { tb1D.selected = false; tb5D.selected = false; tb1M.selected = false; tb3M.selected = false; tb6M.selected = false; tbYTD.selected = false; tb1Y.selected = false; tb5Y.selected = false; tbAll.selected = false; tButton.selected = true; } Creating Live Stock Quote Sample Applications using the Message service /** * Get the maximum and minimum "close" values in the data provider array. * This is used to dynamically change the * axis as the slider values are changed . */ private function getMinMax():Array { var highestClose:int = 0; var lowestClose:int = 0; for (var i:int = 0; i < stockHistory.length; i++) { var point1:StockInTime = stockHistory.getItemAt(i) as StockInTime; if (point1.date.getTime() > timeSlider.values[0] && point1.date.getTime() < timeSlider.values[1]) { lowestClose = point1.close; break; } } for (var j:int = stockHistory.length-1; j >= 0; j--) { var point2:StockInTime = stockHistory.getItemAt(j) as StockInTime; if (point2.date.getTime() > timeSlider.values[0] && point2.date.getTime() < timeSlider.values[1]) { highestClose = point2.close; break; } } return new Array(lowestClose, highestClose); } private function showLoading():void { stockHistoryChart.filters = [blurFilter]; loading.visible=true; } private function removeLoading():void { stockHistoryChart.filters = new Array(); loading.visible=false; } ]]> /fx:Script> <!-- stock history chart --> <mx:AreaChart id="stockHistoryChart" Creating Live Stock Quote Sample Applications using the Message service x="0" y="0" width="883" height="223" dataProvider="{stockHistory}" showDataTips="true"> <!-- seriesFilters="[]"--> <!-- horizontal axis --> <mx:horizontalAxis> <mx:DateTimeAxis id="dtAxis" dataUnits="days" displayLocalTime="true" /> </mx:horizontalAxis> <!-- vertical axis --> <mx:verticalAxis> <mx:LinearAxis id="lAxis"/> <!--minimum="{low}" maximum="{high}"--> </mx:verticalAxis> <!-- series --> <mx:series> <mx:AreaSeries displayName="{symbol}" xField="date" yField="close" alpha="0.8"/> </mx:series> </mx:AreaChart> <!-- ToggleButton bar --> <s:HGroup id="toggleButtonGroup" x="224" y="251" width="436" height="24" paddingLeft="0" paddingRight="0" paddingTop="0" paddingBottom="0" textAlign="center" verticalAlign="middle"> <s:ToggleButton id="tb1D" label="1D" width="43" fontFamily="Verdana" fontSize="10" enabled="false"/> <s:ToggleButton id="tb5D" label="5D" width="43" fontFamily="Verdana" fontSize="10" enabled="false"/> <s:ToggleButton id="tb1M" label="1M" width="43" fontFamily="Verdana" fontSize="10" click="toggleButtonClick(event)"/> <s:ToggleButton id="tb3M" label="3M" width="43" fontFamily="Verdana" fontSize="10" click="toggleButtonClick(event)"/> <s:ToggleButton id="tb6M" label="6M" width="43" fontFamily="Verdana" fontSize="10" click="toggleButtonClick(event)"/> <s:ToggleButton id="tbYTD" label="YTD" width="43" fontFamily="Verdana" fontSize="10" click="toggleButtonClick(event)"/> <s:ToggleButton id="tb1Y" label="1Y" width="43" fontFamily="Verdana" fontSize="10" click="toggleButtonClick(event)"/> <s:ToggleButton id="tb5Y" label="5Y" width="43" fontFamily="Verdana" fontSize="10" click="toggleButtonClick(event)"/> Creating Live Stock Quote Sample Applications using the Message service <s:ToggleButton id="tbAll" label="All" width="43" fontFamily="Verdana" fontSize="10" click="toggleButtonClick(event)"/> </s:HGroup> <mx:HSlider id="timeSlider" x="28" y="227" width="827" thumbCount="2" liveDragging="true" showDataTip="false" maximum="{currentDateRange.getToDate().getTime()}" minimum="{currentDateRange.getFromDate().getTime()}" snapInterval="{dailyStep}" change = "sliderRangeChange(event)" creationComplete = "timeSlider_creationComplete(event)" /> <s:Label id="loading" text="Loading..." visible="false" fontSize="20" color="#FFFFFF" y="{281/2 - loading.height/2}" x="{883/2 - loading.width/2}" /> </s:Group> Create the LiveChart file The liveChart.mxml displays the data within a LineChart control. The column chart control is defined by using a mx:LineChart MXML tag. The data source is an ArrayCollection named stockData, as shown in the following code snippet. <!-- Line Chart --> <mx:LineChart id="linechart1" x="-1" y="0" height="227" width="400" dataProvider="{stockData}" showDataTips="true" seriesFilters="[]"> <!-- horizontal axis --> <mx:horizontalAxis> <mx:DateTimeAxis id="dtAxis" maximum="{maxDate}" Creating Live Stock Quote Sample Applications using the Message service minimum="{minDate}" dataUnits="seconds" displayLocalTime="true" /> </mx:horizontalAxis> <!-- vertical axis --> <mx:verticalAxis> <mx:LinearAxis minimum="{low}" maximum="{high}"/> </mx:verticalAxis> <!-- series --> <mx:series> <mx:LineSeries xField="Time" yField="Last" displayName="{symbol}"> <mx:lineStroke> <mx:SolidColorStroke weight="1"/> </mx:lineStroke> </mx:LineSeries> </mx:series> </mx:LineChart> The stockData array created in the ActionScript section of liveChart.mxml file is a bindable array, as shown in the following code snippet. [Bindable] public var stockData:ArrayCollection; The following code represents the entire liveChart.mxml file. This file is located in a package named Components. <?xml version="1.0" encoding="utf-8"?> <s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" width="400" height="270"> <fx:Declarations> <fx:Date id="minDate" hours="9" minutes="0" seconds="0" milliseconds="0"/> <fx:Date id="maxDate" hours="16" minutes="0" seconds="0" milliseconds="0"/> </fx:Declarations> <fx:Script> <![CDATA[ Creating Live Stock Quote Sample Applications using the Message service import mx.collections.ArrayCollection; /** dataProvider for the chart */ [Bindable] public var stockData:ArrayCollection; /** Day's low. used to set the chart's axis bounds. */ [Bindable] public var low:Number; /** Day's high. used to set the chart's axis bounds. */ [Bindable] public var high:Number; /** Two minutes represented in seconds. Used to set the steps of the slider. */ [Bindable] public var twoMinStep:uint = 1000*60*2; /** Symbol of the currently displayed stock. */ [Bindable] public var symbol:String; /** Change the axis of the chart based on the values of the slider */ private function sliderRangeChange(event:Event):void { dtAxis.minimum = new Date(event.currentTarget.values[0]); dtAxis.maximum = new Date(event.currentTarget.values[1]); } /** Intialize slider thumb positions */ private function timeSlider_creationComplete(event:Event):void { timeSlider.values=[maxDate.getTime(),minDate.getTime()]; } /** Format the dataTip to display the time. */ private function dataTipFormat(ms:Number):String { return new Date(ms).toTimeString(); } ]]> </fx:Script> <!-- Line Chart --> <mx:LineChart id="linechart1" x="-1" y="0" height="227" width="400" dataProvider="{stockData}" showDataTips="true" seriesFilters="[]"> Creating Live Stock Quote Sample Applications using the Message service <!-- horizontal axis --> <mx:horizontalAxis> <mx:DateTimeAxis id="dtAxis" maximum="{maxDate}" minimum="{minDate}" dataUnits="seconds" displayLocalTime="true" /> </mx:horizontalAxis> <!-- vertical axis --> <mx:verticalAxis> <mx:LinearAxis minimum="{low}" maximum="{high}"/> </mx:verticalAxis> <!-- series --> <mx:series> <mx:LineSeries xField="Time" yField="Last" displayName="{symbol}"> <mx:lineStroke> <mx:SolidColorStroke weight="1"/> </mx:lineStroke> </mx:LineSeries> </mx:series> </mx:LineChart> <!-- hSlider for horizontal axis range --> <mx:HSlider id="timeSlider" x="28" y="228" width="362" thumbCount="2" liveDragging="true" maximum="{maxDate.getTime()}" minimum="{minDate.getTime()}" snapInterval="{twoMinStep}" dataTipFormatFunction = "dataTipFormat" change = "sliderRangeChange(event)" creationComplete = "timeSlider_creationComplete(event)" /> <s:Label x="28" y="254" text="Time Range"/> </s:Group> Creating Live Stock Quote Sample Applications using the Message service Create the Stock file The Stock.as file represents a stock message that is retrieved from the stockFeed destination. This class contains a RemoteClass tag that references the Java class named Stock that is located in the stock package. The data members in this class match the data members that are located in the Stock Java class. For information about the Stock Java class, see Understanding the Stock class. The following ActionScript code represents the entire Stock.as file. This class is located in a package named stock. package stock { import mx.collections.ArrayCollection; [RemoteClass(alias="stock.Stock")] [Bindable] public class Stock { public var symbol:String; public var name:String; public var low:Number; public var high:Number; public var open:Number; public var last:Number; public var change:Number = 0; public var tradeTime:Date; public var daily:ArrayCollection = new ArrayCollection(); } } Create the StockInTime file The StockInTime.as file represents a message that is retrieved from the setQuotes destination. This class contains a RemoteClass tag that references the Java class named StockInTime that is located in the stock package. The data members in this class match the data members that are located in the StockInTime Java class. For information about the StockInTime Java class, see Understanding the StockInTime class. The following ActionScript code represents the entire StockInTime.as file. This class is located in a package named stock. package stock Creating Live Stock Quote Sample Applications using the Message service { [RemoteClass(alias="stock.StockInTime")] [Bindable] public class StockInTime { public var close:Number; public var date:Date; } } Create the DateRange file The DateRange.as file represents a date range that is used by the client application. By default the date range is set from one month ago to the present. The following ActionScript code represents the entire DateRange.as file. This class is located in a package named stock. package stock { /** * Object with two dates representing a range. * By default the range is set from * 1 month ago to today. */ public class DateRange { private static const today:Date = new Date(); private var fromDate:Date; private var toDate:Date; public function DateRange() { set5Y(); } public return } public return } function getFromDate():Date { this.fromDate; function getToDate():Date { this.toDate; /** Set range: 1 month ago to today public function set1M():DateRange { this.toDate = today; this.fromDate = substractMonths(1); */ Creating Live Stock Quote Sample Applications using the Message service return this; } /** Set range: 3 months ago to today public function set3M():DateRange { this.toDate = today; this.fromDate = substractMonths(3); return this; } */ /** Set range: 6 months ago to today public function set6M():DateRange { this.toDate = today; this.fromDate = substractMonths(6); return this; } */ /** Set range: January 1 this year to today public function setYTD():DateRange { this.toDate = today; this.fromDate = new Date(); this.fromDate.month = 0; return this; } */ /** Set range: 1 year ago to today */ public function set1Y():DateRange { this.toDate = today; this.fromDate = substractMonths(12); return this; } /** Set range: 5 years ago to today */ public function set5Y():DateRange { this.toDate = today; this.fromDate = substractMonths(5*12); return this; } /** Set range: 1/1/1970 to today */ public function setAll():DateRange { this.toDate = today; this.fromDate = new Date(0); return this; } Creating Live Stock Quote Sample Applications using the Message service /** Set range between indicated dates */ public function setCustom(from:Date, to:Date):DateRange { this.toDate = to; this.fromDate = from; return this; } /** * Substracts a given number of months from a given date. Returns * Date object with "aNumber" of months less. If no date is specified, * the current date is used. */ private function substractMonths(aNumber:int, aDate:Date = null):Date { var toReturn:Date; var months:int; var years:int; if (aDate == null) { toReturn = new Date(today.getTime()); months = today.month; years = today.fullYear; } else { toReturn = new Date(aDate.getTime()); months = aDate.month; years = aDate.fullYear; } months -=aNumber; while (months < 0) { years -= 1; months += 12; } toReturn.month = months; toReturn.fullYear = years; return toReturn; } } } Create the JSP that starts the server thread Before the client application can display financial data, the server thread must be started. To start the server thread, a JSP is created, whose only purpose is to start the server Creating Live Stock Quote Sample Applications using the Message service thread. For information about the server thread, see Understanding the StockQuoteFeed class. The JPS page imports the feed.StockQuoteFeed class and calls its start method. The following code represents the JSP named startfeed.jps that starts the server thread. <%@page import="feed.StockQuoteFeed"%> <% try { StockQuoteFeed feed = new StockQuoteFeed(); feed.start(); out.println("StockQuoteFeed Started"); } catch (Exception e) { out.println("A problem occured while starting the feed: "+e.getMessage()); } %> This JSP can be deployed the root of your web application. For example, assume that the name of your web application is named stockDashBoard. In this situation, deploy the JSP to the following location. [Install directory]lcds\tomcat\webapps\stockDashboard where [Install directory] is the directory on which LiveCycle Data Services is installed. To start the server thread, enter the following URL into a web browser. http://[server]:[port]/stockDashboard/startfeed.jsp Stopping the server thread You can also create a JPS that stops the server thread. The JPS page imports the feed.StockQuoteFeed class and calls its stop method. The following code represents the JSP named stopfeed.jps that stops the server thread. <%@page import="feed.StockQuoteFeed"%> <% try { StockQuoteFeed feed = new StockQuoteFeed(); feed.stop(); out.println("StockQuoteFeed Stopped"); } catch (Exception e) { out.println("A problem occured while starting the feed: "+e.getMessage()); } %> Creating Live Stock Quote Sample Applications using the Message service This JSP can be deployed to the root of your web application. To stop the server thread, enter the following URL into a web browser. http://[server]:[port]/stockDashboard/stopfeed.jsp After you start the server thread, you can run the client application and enter a stock symbol and click the Add Symbol button. The data is displayed in the client application. Click a row of data to view additional details. In time, data is plotted in the Line chart control. Creating Live Stock Quote Sample Applications using the Message service
© Copyright 2024