Friday, November 21, 2008

An AIR based Woot-Off tracker

I've been going through some Flex training this week. It's an interesting tool, and pretty easy to make a quick application. Unfortunately, the training has been a bit robotic in terms of being very prescriptive on how to perform somewhat elementary programming. So it was time to take a break and actually try to attempt something that would be useful....or at least somewhat useful.

Since we had a Woot-Off yesterday, I decided to use Flex to write a Woot-Off tracker. A handy little AIR application to see when a new item appears. It's a simple single windowed application that polls Woot's API every 30 seconds, groks the RSSish feed, and displays information about the item being sold.

In addition, it provides handy information regarding the status of the sale in terms of percentage of items sold and a button to purchase the product, or manually check Woot for an update. If the percentage sold is above 94%, it ramps up the polling process to check Woot every second, since you never know when the BOC will appear.

The application is hardly complete. It lacks any style or substance in terms of look and feel. It also neglects the ability to run in the system tray (ala Twhirl or Tweetdeck) and update the user that an item might be selling out soon or that a new item is available. Right now it simply runs on the screen.

Of course, the trickiest part of this application is the need to run a Proxy service to hit an external URL. Due to Flash's security model, and the lack of a crossdomain.xml file at Woot, you need to have a local service running that will act as a proxy. A quick Java servlet and the very very lightweight Winstone servlet container. Ideally, you would launch this app with a little batch script that spun up your Servlet based proxy and then spun up the AIR app.

So let's walk through the source. That way, all of you out there who've actually done a lot of Flex and look at this and let me know what a BOC it is. :D First we'll look at the AIR app, and finally the Java based Proxy.


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
creationComplete="init()" width="500" height="370" xmlns:utils="flash.utils.*">
<mx:Script>
<![CDATA[
import flash.net.navigateToURL;
import flash.utils.Timer;
import mx.formatters.Formatter;
import mx.formatters.NumberFormatter;
import mx.collections.ArrayCollection;
import mx.controls.Image;
import mx.rpc.events.ResultEvent;

private var START_QUICK_POLL_PERCENT:Number = 0.94;

// a 30 second and a 1 second Timer
private var wootPing:Timer = new Timer(30000, 1000000000);
private var wootEndPing:Timer = new Timer(1000, 1000000000);
private var checks:int = 0;

[Bindable] private var wootItemText:String = "No Woot Found...Yet";
[Bindable] private var wootItemPrice:String = "$10,000,000.00";
[Bindable] private var wootItemPercent:String = "0% Sold";
[Bindable] private var wootItemLink:String = "http://www.woot.com";
[Bindable] private var checkText:String= "Checked\n0 Times";
[Bindable] private var itemImgURL:String = "";

// Setup the Timers, and start the default timer
private function init():void {
wootPing.addEventListener(Event.ACTIVATE, wootHandler);
wootPing.addEventListener(TimerEvent.TIMER, wootHandler);
wootPing.start();
wootEndPing.addEventListener(TimerEvent.TIMER, wootHandler);
}

// Handles the purchase button to open your browser
private function openWootWindow(event:MouseEvent):void {
var u:URLRequest = new URLRequest(wootItemLink);
flash.net.navigateToURL(u, "_blank");
}

// Generally use the Event to handle updating the app
private function wootHandler(event:Event):void {
getWoot();
}

// Hits the API.
private function getWoot():void {
wootService.send();
checkCount.text = "Checked\n"+ ++checks + " times";
}

// Updates all of the items when the HTTPService completes
private function wootResultHandler(event:ResultEvent):void {
wootItemText = wootService.lastResult.rss.channel.item.title;
wootItemPrice = wootService.lastResult.rss.channel.item.price;
wootItemLink = wootService.lastResult.rss.channel.item.purchaseurl;
wootDesc.htmlText = wootService.lastResult.rss.channel.item.description;
wootImage.source = wootService.lastResult.rss.channel.item.thumbnailimage;
// Determine if we need to do percentage checking.
if (!(new Boolean(wootService.lastResult.rss.channel.item.wootoff))) {
wootItemPercent = "This is not a Woot-Off";
} else {
var percentNum:Number = new Number(wootService.lastResult.rss.channel.item.soldoutpercentage);
wootItemPercent = new String(percentNum/100 + "% Sold");
// Do we need to start checking more often?
if (!wootEndPing.running && percentNum > START_QUICK_POLL_PERCENT) {
wootEndPing.start();
} else if (wootEndPing.running && percentNum < START_QUICK_POLL_PERCENT) {
wootEndPing.stop();
}
}
}
]]>
</mx:Script>
<mx:HTTPService url="http://localhost:8080/WootProxy"
id="wootService" result="wootResultHandler(event)" />

<mx:VBox left="5" right="5" top="5" bottom="5">
<mx:Label id="itemText" text="{wootItemText}" fontSize="12" fontWeight="bold"/>
<mx:Canvas width="100%">
<mx:Button toolTip="Click to purchase" label="Purchase" click="openWootWindow(event)" y="152" x="0"/>
<mx:TextArea height="300" id="wootDesc" left="150" right="0" />
<mx:Image id="wootImage" width="142" height="116" left="0" top="0" >
<mx:source>http://upload.wikimedia.org/wikipedia/en/1/16/Wootlogo.png</mx:source>
</mx:Image>
<mx:Label id="checkCount" text="{checkText}" x="0" y="212" height="52" width="142"/>
<mx:Button toolTip="Click to load Woot" label="Check" click="getWoot()" y="182" />
<mx:Label id="itemPrice" text="{wootItemPrice}" y="124" fontStyle="italic" fontSize="12" x="0" width="142"/>
<mx:Label id="itemPercent" text="{wootItemPercent}" y="272" x="0" width="142"/>
</mx:Canvas>
</mx:VBox>
</mx:WindowedApplication>

And finally the Proxy:

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.woot.tracker;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
*
* @author student
*/
public class WootProxy extends HttpServlet {

static final long serialVersionUID = 1L;

/**
* Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

String contentObj = "http://www.woot.com/salerss.aspx";

URL content = null;

if (null == contentObj) {
throw new ServletException("The destination url must be specified for ProxyHttpService");
}

try {
content = new URL(contentObj);
} catch (MalformedURLException e) {
throw new ServletException(contentObj + " is a malformed url.");
}
HttpURLConnection contentCon = null;
try {
contentCon = (HttpURLConnection) content.openConnection();
} catch (IOException exception) {
throw new ServletException("Problem opening " + contentObj + ": " + exception.toString());
}

// Get the content type from the URLConnection and set it on the response.
String contentType = contentCon.getContentType();
response.setContentType(contentType);

// Get and read the input stream.
StringBuffer buffer = new StringBuffer();

BufferedReader din =
new BufferedReader(new InputStreamReader(contentCon.getInputStream()));

String s;
while ((s = din.readLine()) != null) {
buffer.append(s);
}
din.close();

// Now write the bytes out to the client.
byte[] contentBytes = buffer.toString().getBytes();
OutputStream out = response.getOutputStream();
out.write(contentBytes, 0, contentBytes.length);
out.flush();
out.close();
}

// <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
/**
* Handles the HTTP <code>GET</code> method.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

/**
* Handles the HTTP <code>POST</code> method.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

/**
* Returns a short description of the servlet.
* @return a String containing servlet description
*/
@Override
public String getServletInfo() {
return "Short description";
}// </editor-fold>
}
There it is, enjoy :)

No comments:

ShareThis