I’ve been working on Rife, a Google Reader client, over the last few days and have been digging my way through some more HotCocoa mappings. I figured the best way to remember some of this stuff is to write it down so, the following will look at synchronous and asynchronous downloads and writing a XML parser in HotCocoa/MacRuby.

So, what are we creating you ask? Well, as I said, I’ve been playing with Google Reader APIs and we’re going to do a synchronous request to Google for an identifier token. Once we’ve successfully authenticated we’re going to make an asynchronous request for our unread items. We’ll then parse the resulting XML document and spit the titles out to the console.

As with any HotCocoa application the easiest way to get started is to have system setup the shell of our application. We’ll use the hotcocoa command to create our application which I’m calling titles.

titania:example dj2$ hotcocoa titles

In order to authenticate to Google we’re going to need your username and password. Since I’m going to do the output to the console for demonstration purposes I’ll use the main application window to show the fields for username and password and a save button.

require 'rubygems'
require 'hotcocoa'

class Application
  include HotCocoa
  
  def start
    application(:name => "Titles") do |app|
      app.delegate = self

      window(:frame => [100, 100, 200, 200], :title => "Titles") do |win|
        win.center
        win.will_close { exit }

        win << label(:text => "Username", :layout => {:start => false})
        win << @username_field = text_field(:layout => {:start => false, :expand => [:width]})

        win << label(:text => "Password", :layout => {:start => false})
        win << @password_field = secure_text_field(:layout => {:start => false, :expand => [:width]})

        win << save = button(:title => "save", :layout => {:start => true}) do |button|
          button.on_action { authenticate }
        end

        @username_field.setNextKeyView(@password_field)
        @password_field.setNextKeyView(save)
        save.setNextKeyView(@username_field)
      end
    end
  end

  def authenticate
    puts "DO AUTH #{@username_field.to_s} #{@password_field.to_s}"
  end
end

Application.new.start

If you run the application by executing macrake in the titles directory you should see the main application window. Typing something into the username and password fields and pressing save you should see something similar to the following in your terminal.

DO AUTH test test

With our framework setup let’s get to the interesting stuff. First up, authenticating so we can retrieve our identifier from Google. We’re going to make a synchronous request to retrieve the identifier and, if successful, call a method to start retrieving our reading list.

def authenticate
  username = @username_field.stringValue
  password = @password_field.stringValue

  query = "https://www.google.com/accounts/ClientLogin?" +
          "Email=#{CGI.escape(username)}&Passwd=#{CGI.escape(password.to_s)}" +
          "&source=HotCocoaExample&service=reader"

  url = NSURL.URLWithString(query)
  request = NSMutableURLRequest.requestWithURL(url)
  request.addValue("HotCocoaExample", forHTTPHeaderField:"source")
  request.addValue("2", forHTTPHeaderField:"GData-Version")

  response = Pointer.new("@")
  data = NSURLConnection.sendSynchronousRequest(request, returningResponse:response, error:nil)
  data = NSString.alloc.initWithData(data, encoding:NSUTF8StringEncoding)

  if data =~ /^SID=(.*)\n/
    @sid = $1

    retrieve_reading_list
  else
    raise Exception.new("Authentication failed with: #{data}")
  end
end

def retrieve_reading_list
  puts @sid
end

If you add the above to your application, and add require 'cgi', you should be able to run the program, put in your username and password and get a long line of characters spit out on the terminal. Those characters are your Google SID.

Let’s look a bit closer at what we’re doing in the authenticate method. We start by grabbing the stringValue for the username and password fields. Then, using these values, we build the query string needed for authentication. This query string is used to build a URL object by calling NSURL.URLWithString(query). With the URL in hand we can start building our request object. This is done by calling NSMutableURLRequest.requestWithURL(url). I’m using the mutable version of the request as I want to add a few extra header values. These are both added with addValue(value, forHTTPHeaderField:field).

When we execute our request the system is going to want to put our response object somewhere. In the Cocoa version the method accepts a NSURLResponse \*\*response parameter. In order to handle the response we need to create a Pointer object which is a MacRuby object for handling these pointers to objects. We want our pointer to point to an object so we use Pointer.new("@").

With the response setup we call NSURLConnection.sendSynchronousRequest and provide our request and response objects. I don’t care about the error, but if you do, you’d want to pass in something similar to our response pointer. The request will return a NSData object which we convert to a string using the initWithData initialization method of NSString.

With the string in hand we try to extract our SID and, if successful, execute the retrieve_reading_list method which just spits out the SID.

OK, cool, we’ve now got our authentication token and are ready to move onto the asynchronous request to get our reading list.

def retrieve_reading_list
  query = "https://www.google.com/reader/atom/user/-/state/com.google/reading-list?" +
          "xt=user/-/state/com.google/read&ck=#{Time.now.to_i * 1000}&n=2"

  url = NSURL.URLWithString(query)
  request = NSMutableURLRequest.requestWithURL(url)
  request.addValue("HotCocoaExample", forHTTPHeaderField:"source")
  request.addValue("2", forHTTPHeaderField:"GData-Version")
  request.addValue("SID=#{@sid}", forHTTPHeaderField:"Cookie")

  NSURLConnection.connectionWithRequest(request, delegate:self)
end

def connectionDidFinishLoading(conn)
  puts NSString.alloc.initWithData(@receivedData, encoding:NSUTF8StringEncoding)
end

def connection(conn, didReceiveResponse:response)
  if response.statusCode != 200
    puts "BAD STATUS: #{response.statusCode}"
    p response.allHeaderFields
  end
end

def connection(conn, didReceiveData:data)
  @receivedData ||= NSMutableData.new
  @receivedData.appendData(data)
end

Similar to the synchronous method we start by building our query string, NSURL and NSMutableURLRequest. We’ve added a cookie to our request object to hold the SID retrieved earlier from Google.

We fire the request by calling NSURLConnection.connectionWithRequest(request, delegate:self). We specific ourselves as the delegate for the connection. There are a few delegate methods we can implement to receive data and get notified of request states. These are:

  • connectionDidFinishLoading(connection)
  • connection(connection, didReceiveResponse:response)
  • connection(connection, didReceiveData:data)

We’ll look at our implementation of each of these callbacks in turn. First, in connectionDidFinishLoading(conn) we’re just printing out the data retrieved. We need to convert the data, similar to what we did in the synchronous request, from a NSData object to a NSString object.

In connection(conn, didReceiveResponse:response) we’re just checking to see if we got a 200 response code from the server. In all other cases we print an error.

The main work is done in connection(conn, didReceiveData:data) where we create a NSMutableData object if needed and append any data received into the mutable data object.

Running the code at this point should dump the first two items in your reading list to the console. The data will be a big mess of XML but we’ll look at parsing that in the next step.

def connectionDidFinishLoading(conn)
  xml = HotCocoa.xml_parser(:data => @receivedData)
  @receivedData = nil

  xml.on_start_document { puts "Starting Parse" }
  xml.on_end_document do
    HotCocoa.notification(:post => true, :name => "all_entries_loaded", :object => nil, :info => nil)
  end
  xml.on_parse_error { |err| puts "Parse error #{err.inspect}" }

  xml.on_cdata { |cdata| @elem_text += cdata.to_s }
  xml.on_characters { |chars| @elem_text += chars.to_s }

  xml.on_start_element do |element, namespace, qualified_name, attributes|
    @elem_text = ''
  end

  xml.on_end_element do |element, namespace, qualified_name|
    puts @elem_text if element == 'title'
  end

  xml.parse
end

We’re finally getting into some HotCocoa specific code with our XML parser. HotCocoa defines a mapping wrapper around NSXMLParser and provides a set of delegate methods. These delegates mean we don’t have to set our class as the delegate and create a bunch of methods. They mean we can attach our code as blocks on our XML object. All the better if you want to define a few parsers in one class.

We start off by creating a HotCocoa.xml_parser. The parser accepts NSData objects so we don’t need to convert our response data to a string. We then setup eight callbacks. There are actually a bunch more callbacks that can be hooked up and you should look at the xml_parser mapping code to see if you need any of them. For our purposes, we only really care about eight.

The on_start_document, on_end_document and on_parse_error callbacks, as you can probably guess, get called when we start parsing, when we finish parsing and when we receive a parse error, respectively. We don’t really care about start in this example, but I put it in anyway. When we’ve completed parsing we send a notification and other application code can then listen for this notification and do anything it needs. If we wanted we could store the entries as they’re parsed and provide them to the :object key. This would make those entries available to anyone that receives the notification.

If we receive either CDATA, with on_cdata, or text, with on_characters, we append the content to our current elements text. When we receive the open tag of a new element, on_start_element, we dump our current element text as we’ve started a new element. We can also take a look at the elements name, attributes, namespace and qualified name, if desired.

Finally, in on_end_element we print out the current element text if the element we’re finishing has a name of title.

With all the callbacks configured we use xml.parse to start the parser. You should, if you run this example, see the titles and authors of the first two posts in your reading list. (The author name is also called title and I’m not bothering to check that the parent element is entry before spitting it out.)

That’s it. You can now make synchronous and asynchronous requests for content and parse any resulting XML.

One last thing before you go. Both of the requests we did above were GET requests. You can do other types of requests using the same methods as above you just need a slightly different setup for the request. You can see a POST request below.

request = NSMutableURLRequest.requestWithURL(url)
request.addValue(SOURCE, forHTTPHeaderField:"source")
request.addValue("2", forHTTPHeaderField:"GData-Version")
request.addValue("SID=#{@sid}", forHTTPHeaderField:"Cookie")

body="first=1&second=2&third=3"

request.setHTTPMethod('POST')
request.setValue('application/x-www-form-urlencoded', forHTTPHeaderField:'Content-Type')
request.setValue(body.length.to_s, forHTTPHeaderField:'Content-Length')
request.setHTTPBody(body.dataUsingEncoding(NSASCIIStringEncoding))

The first few lines should look familiar from creating our asynchronous request above. Since we’re going to be posting the data we use setHTTPMethod('POST') to setup the request method. We’ve form encoded the data so we set the appropriate Content-Type and set the Content-Length. Note, we convert the length to a string before sending to setValue. Finally, we set the body of the post with setHTTPBody. You need to convert the body string into a NSData object which we do with the dataUsingEncoding method. If you don’t convert the body to NSData you’ll end up sending a nil body with your post request.