Using CouchDB in Flex AIR with RestfulX

rxcfc edited this page Aug 14, 2010 · 7 revisions

Using CouchDB in AIR

RestfulX framework allows you to develop standalone Flex and AIR applications that integrate directly with CouchDB using the same set of high-level REST concepts you use when talking to Ruby on Rails or SQLite that comes with AIR.

Standard RestfulX tutorials always seem to start with developing a Flex application. Let’s break with tradition and create an AIR application first this time.

While it’s possible to write the entire application by hand, you can use a few handy generators that come with restfulx gem to help you get up and running faster. Note, there’s no need to install/or run Rails, Merb, etc. Ruby is a great language to use for code generation. So it is used to create a basic Rakefile and add a few RubiGen based generators to help you along the way. It is possible to open/edit any of these projects in Flex Builder 3 if you want to. In fact, when you use rx-gen generator project files for Flex Builder 3 and TextMate are created by default.

What you’ll need:

  1. CouchDB 0.8.1 + (Best of luck if you are building this from source on something other than Linux :))
  2. Flex SDK 3.0 +
  3. Ruby 1.8.6 + (for code generation, not required to use/run the application)

Before you start you might want to add Flex SDK bin folder to your $PATH variable if you haven’t already. This will allow you to invoke commands such as mxmlc from the command line and run rake tasks such as air:run and rx:air:build.

  • On OS X it’s typically /Applications/Adobe Flex Builder 3/sdks/3.0.0/bin
  • On Win32 it’s C:\Program Files\Adobe\Flex Builder 3\sdks\3.0.0\bin

You will also want to start your CouchDB server.

1. Creat a project

First, let’s create a project:


$>sudo gem install restfulx
$>rx-gen -a pomodo
$>cd pomodo

2. Get a build of the framework


$>./script/generate rx_config

3. Create a model.yml file


$>touch db/model.yml

Edit db/model.yml to contain the following:


project:
 - name: string
 - notes: text
 - start_date: date
 - end_date: date
 - completed: boolean
 - belongs_to: [user]
 - has_many: [tasks]

location:
 - name: string
 - notes: text
 - belongs_to: [user]
 - has_many: [tasks]

task:
 - name: string
 - notes: text
 - start_time: datetime
 - end_time: datetime
 - completed: boolean
 - next_action: boolean
 - belongs_to: [project, location, user]

note:
 - content: text
 - belongs_to: [user]

user:
 - login: string
 - first_name: string
 - last_name: string
 - email: string
 - has_many: [tasks, projects, locations]
 - has_one: [note]

4. Generate the application


$>./script/generate rx_yaml_scaffold

At this point you have a standalone AIR application that is configured to use local SQLite database that comes with AIR.

5. Try it out


$>rake air:run

Go ahead and create some records!

6. Switching to CouchDB

If you look at Pomodo.mxml file generated for you, it should look like this:


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
  xmlns:generated="pomodo.views.generated.*"
  paddingBottom="8" paddingLeft="8" paddingRight="8" paddingTop="8"
  layout="horizontal" styleName="plain" initialize="init()">
  <mx:Script>
    <![CDATA[
      import org.restfulx.services.air.AIRServiceProvider;
      import org.restfulx.services.http.XMLHTTPServiceProvider;
      import org.restfulx.Rx;
      import pomodo.controllers.ApplicationController;

      private function init():void {
        ApplicationController.initialize([AIRServiceProvider], 
          AIRServiceProvider.ID, "pomodo");
      }
    ]]>
  </mx:Script>
  <mx:LinkBar dataProvider="{mainViewStack}" direction="vertical" borderStyle="solid" backgroundColor="#EEEEEE"/>
  <mx:ViewStack id="mainViewStack" width="100%" height="100%">
    <!-- For a simple demo, put all the components here. -->
    <generated:ScrapBox/>
    <generated:UserBox/>
  </mx:ViewStack>
</mx:WindowedApplication>

What we are going to do is change it to this:


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
  xmlns:generated="pomodo.views.generated.*"
  paddingBottom="8" paddingLeft="8" paddingRight="8" paddingTop="8"
  layout="horizontal" styleName="plain" initialize="init()">
  <mx:Script>
    <![CDATA[
      import org.restfulx.services.as3http.DirectCouchDBHTTPServiceProvider;
      import org.restfulx.services.http.XMLHTTPServiceProvider;
      import org.restfulx.Rx;
      import pomodo.controllers.ApplicationController;

      private function init():void {
        ApplicationController.initialize([DirectCouchDBHTTPServiceProvider], DirectCouchDBHTTPServiceProvider.ID);
      }
    ]]>
  </mx:Script>
  <mx:LinkBar dataProvider="{mainViewStack}" direction="vertical" borderStyle="solid" backgroundColor="#EEEEEE"/>
  <mx:ViewStack id="mainViewStack" width="100%" height="100%">
    <!-- For a simple demo, put all the components here. -->
    <generated:ScrapBox/>
    <generated:UserBox/>
  </mx:ViewStack>
</mx:WindowedApplication>

By default RestfulX framework will try to connect to CouchDB installation at this address: http://127.0.0.1:5984/. If you configured your CouchDB instance differently remember to set Rx.couchDBRootUrl appropriately.

Default database name used by the framework is rxdb. If you want to change this set Rx.couchDbDatabaseName to something like this Rx.couchDbDatabaseName = "foobar";

7. You are done!

Not quite :).

You need to actually create the database if it doesn’t exist. Navigate to http://127.0.0.1:5984/_utils/ and create a new database. If you stick with RestfulX defaults call the database rxdb.

With the database created, simply run:


$>rake air:run

Congrats you are now using CouchDB for persistence from your AIR app. Go ahead and create some stuff!

Using CouchDB in Flex

OK, now that we have our AIR application talking to CouchDB, we are going to turn it into a Flex app.

1. Convert the application

Simply run the following command in your pomodo application folder and type y whenever prompted to overwrite files:


$>rx-gen .

2. Switch to CouchDB, again…

Let’s run ./script/generate rx_yaml_scaffold again to re-generate Pomodo.mxml file.


$>./script/generate rx_yaml_scaffold

If you look at Pomodo.mxml file re-generated for you, it should look like this:


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
  xmlns:generated="pomodo.components.generated.*"
  paddingBottom="8" paddingLeft="8" paddingRight="8" paddingTop="8"
  layout="horizontal" styleName="plain" initialize="init()">
  <mx:Script>
    <![CDATA[
      import pomodo.controllers.ApplicationController;

      private function init():void {
        ApplicationController.initialize();
      }
    ]]>
  </mx:Script>
  <mx:LinkBar dataProvider="{mainViewStack}" direction="vertical" borderStyle="solid" backgroundColor="#EEEEEE"/>
  <mx:ViewStack id="mainViewStack" width="100%" height="100%">
    <!-- For a simple demo, put all the components here. -->
    <generated:LocationBox/>
    <generated:NoteBox/>
    <generated:ProjectBox/>
    <generated:TaskBox/>
    <generated:UserBox/>
  </mx:ViewStack>
</mx:Application>

We are going to change it to this:


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
  xmlns:generated="pomodo.components.generated.*"
  paddingBottom="8" paddingLeft="8" paddingRight="8" paddingTop="8"
  layout="horizontal" styleName="plain" initialize="init()">
  <mx:Script>
    <![CDATA[
      import org.restfulx.services.as3http.DirectCouchDBHTTPServiceProvider;
      import pomodo.controllers.ApplicationController;

      private function init():void {
        ApplicationController.initialize([DirectCouchDBHTTPServiceProvider], DirectCouchDBHTTPServiceProvider.ID);
      }
    ]]>
  </mx:Script>
  <mx:LinkBar dataProvider="{mainViewStack}" direction="vertical" borderStyle="solid" backgroundColor="#EEEEEE"/>
  <mx:ViewStack id="mainViewStack" width="100%" height="100%">
    <!-- For a simple demo, put all the components here. -->
    <generated:LocationBox/>
    <generated:NoteBox/>
    <generated:ProjectBox/>
    <generated:TaskBox/>
    <generated:UserBox/>
  </mx:ViewStack>
</mx:Application>

This should look familiar. In fact, we’ve done the exact same thing in our AIR appliction. We simply switched the service provider to CouchDB.

3. You are done!

Again, not quite :). First you need to build the application:


$>rake rx:flex:build

That was the easy part.

Now we need a web server of some kind to serve our application. I’m lazy so I am going to use ruby for this. Here’s my little server:


require "rubygems"
require "sinatra"

begin
  require 'thin'
rescue LoadError
  puts 'Lose a little weight maybe?'
end

get '/' do
  redirect '/index.html'
end

You can save it to any file you want (e.g. pomodo.rb). Start it by typing ruby pomodo.rb.

Now we need to deal with some funky crossdomain.xml issues. Since our Flex application is going to be making requests to a domain different than its own (CouchDB server running on a different port). We need something called Socket Policy Server to make progress. All that server does is return appropriate crossdomain.xml file everytime it’s required. You can refer to: http://www.adobe.com/devnet/flashplayer/articles/socket_policy_files.html for details.

Here’s what our crossdomain.xml will look like:


<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <site-control permitted-cross-domain-policies="master-only" />
  <allow-access-from domain="*" to-ports="5984" />
</cross-domain-policy>

Now we need an actual policy server. There’s a few different configurations refered to in the Adobe DevNet article. I’ve used their standalone Python configuration in this case. Here’s the code for your convenience, you can also get it from the article.


#!/usr/bin/env python
#
# flashpolicyd.py
# Simple socket policy file server for Flash
#
# Usage: flashpolicyd.py [--port=N] --file=FILE
#
# Logs to stderr
# Requires Python 2.5 or later

from __future__ import with_statement

import sys
import optparse
import socket
import thread
import exceptions
import contextlib

VERSION = 0.1

class policy_server(object):
    def __init__(self, port, path):
        self.port = port
        self.path = path
        self.policy = self.read_policy(path)
        self.log('Listening on port %d\n' % port)
        try:
            self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
        except AttributeError:
            # AttributeError catches Python built without IPv6
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        except socket.error:
            # socket.error catches OS with IPv6 disabled
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind(('', port))
        self.sock.listen(5)
    def read_policy(self, path):
        with file(path, 'rb') as f:
            policy = f.read(10001)
            if len(policy) > 10000:
                raise exceptions.RuntimeError('File probably too large to be a policy file',
                                              path)
            if 'cross-domain-policy' not in policy:
                raise exceptions.RuntimeError('Not a valid policy file',
                                              path)
            return policy
    def run(self):
        try:
            while True:
                thread.start_new_thread(self.handle, self.sock.accept())
        except socket.error, e:
            self.log('Error accepting connection: %s' % (e[1],))
    def handle(self, conn, addr):
        addrstr = '%s:%s' % (addr[0],addr[1])
        try:
            self.log('Connection from %s' % (addrstr,))
            with contextlib.closing(conn):
                # It's possible that we won't get the entire request in
                # a single recv, but very unlikely.
                request = conn.recv(1024).strip()
                if request != '<policy-file-request/>\0':
                    self.log('Unrecognized request from %s: %s' % (addrstr, request))
                    return
                self.log('Valid request received from %s' % (addrstr,))
                conn.sendall(self.policy)
                self.log('Sent policy file to %s' % (addrstr,))
        except socket.error, e:
            self.log('Error handling connection from %s: %s' % (addrstr, e[1]))
        except Exception, e:
            self.log('Error handling connection from %s: %s' % (addrstr, e[1]))
    def log(self, str):
        print >>sys.stderr, str

def main():
    parser = optparse.OptionParser(usage = '%prog [--port=PORT] --file=FILE',
                                   version='%prog ' + str(VERSION))
    parser.add_option('-p', '--port', dest='port', type=int, default=843,
                      help='listen on port PORT', metavar='PORT')
    parser.add_option('-f', '--file', dest='path',
                      help='server policy file FILE', metavar='FILE')
    opts, args = parser.parse_args()
    if args:
        parser.error('No arguments are needed. See help.')
    if not opts.path:
        parser.error('File must be specified. See help.')

    try:
        policy_server(opts.port, opts.path).run()
    except Exception, e:
        print >> sys.stderr, e
        sys.exit(1)
    except KeyboardInterrupt:
        pass

if __name__ == '__main__':
    main()

Save this to any file (e.g. fps.py) and start it using:


$>sudo python fps.py --file=crossdomain.xml

You’ll need to be super-user to run this (if you are on Linux/Mac) because Flash Policy Server is expected to run on port 843, which is privileged.

If you have both our little Sinatra server and the Flash Policy Server running, navigate to http://localhost:4567 and play with our Flex+CouchDB application.

Now you are really done.

While code generation is a quick way to get started, it falls far short of exposing you to all the things you can do with the framework. Examine the code you just generated and refer to Working with RestfulX Models for more information on what you can accomplish.

If you have any questions get in touch at RestfulX Framework Google Group

For more information on the RestfulX framework refer to Home.

Want a more feature complete example? Check out Pomodo On Rails

Adding authentication and/or authorization is left as an exercise to the reader :). OAuth anyone?

See Soup for a simple AIR/CouchDB/RestfulX example that features synchronization and undo/redo support.