Dima Berastau

Working with RestfulX Model Attachments

19 Mar 2009 – Vancouver, BC

Getting Started

In this tutorial we’ll explore how you can take advantage of RestfulX support for model attachments to upload files or other binary data to the server.

It assumes that you have a functional Ruby/Rails/Flex/RestfulX development environment. If you are not sure, please refer to Getting Started with RestfulX and Ruby on Rails for a few set-up instructions.

Creating Rails application

The first thing we’ll need to do is create our Rails application of course.

$>rails rx_model_attachments

The next thing we need to do is add restfulx and paperclip gems to our application configuration. Make sure you install these gems if you haven’t done so already.

$>sudo gem install paperclip
$>sudo gem install restfulx

Let’s edit config/environment.rb.

@@ -27,6 +27,8 @@ Rails::Initializer.run do |config|
   # config.gem "sqlite3-ruby", :lib => "sqlite3"
   # config.gem "aws-s3", :lib => "aws/s3"
+  config.gem "restfulx"
+  config.gem "paperclip"
 
   # Only load the plugins named here, in the order given. By default, all plugins 
   # in vendor/plugins are loaded in alphabetical order.

Now we can run ./script/generate rx_config to set up the default RestfulX configuration.

RestfulX Configuration

Running that command will create a few files. While we can skip most of them, there’s a few generated files that we should review before going forward.

The main application file RxModelAttachments.mxml will look like this by default.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
  xmlns:generated="rxmodelattachments.components.generated.*"
  paddingBottom="8" paddingLeft="8" paddingRight="8" paddingTop="8"
  layout="horizontal" styleName="plain" initialize="init()">
  <mx:Script>
    <![CDATA[
      import org.restfulx.Rx;
      import rxmodelattachments.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. -->
  </mx:ViewStack>
</mx:Application>

As you can see, there’s not much to it. The important thing is the ApplicationController.initialize() function call that will configure RestfulX for this application.

The default ApplicationController is created in ApplicationController.as and looks like this.

package rxmodelattachments.controllers {
  import rxmodelattachments.models.*;
  import rxmodelattachments.commands.*;

  import mx.core.Application;    
  import org.restfulx.Rx;
  import org.restfulx.controllers.RxApplicationController;
  import org.restfulx.utils.RxUtils;

  public class ApplicationController extends RxApplicationController {
    private static var controller:ApplicationController;
    
    public static var models:Array = []; /* Models */
    
    public static var commands:Array = []; /* Commands */
    
    public function ApplicationController(enforcer:SingletonEnforcer, 
      extraServices:Array, defaultServiceId:int = -1) {
      super(commands, models, extraServices, defaultServiceId);
    }
    
    public static function get instance():ApplicationController {
      if (controller == null) initialize();
      return controller;
    }
    
    public static function initialize(extraServices:Array = null, 
      defaultServiceId:int = -1, airDatabaseName:String = null):void {
      if (!RxUtils.isEmpty(airDatabaseName)) Rx.airDatabaseName = airDatabaseName;
      controller = new ApplicationController(new SingletonEnforcer, 
        extraServices, defaultServiceId);
      Rx.sessionToken = Application.application.parameters.session_token;
      Rx.authenticityToken = Application.application.parameters.authenticity_token;
    }
  }
}

class SingletonEnforcer {}

This will hooks up various models, commands and other configuration options for the app.

Generating Contacts

Now that we have a working set-up, it’s time to add some functionality! We’ll start by creating a model.yml file that describes our data model. This file is used to generate appropriate Flex and Rails models, controllers, etc, out of the box, so that you don’t have to go and manually edit anything to get started. Here’s what our model.yml file looks like for this app.

contact:
 - first_name: string
 - last_name: string
 - email_address: string
 - notes: text
 - attachment_field: [avatar]

Pretty simple right?

contact is the name of our model and all the lines prefixed with - denote attributes. Things like first_name and last_name name are simple properties, while attachment_field is a special RestfulX property that will be used to generate appropriate configuration for uploading files. By default, it is based on paperclip and will use attachment name of avatar in your ActiveRecord model.

The next thing we need to do is generate the app. We can do that by running ./script/generate rx_yaml_scaffold

Generating Application

Again, this will create a number of files, including ActiveRecord migration, ActiveRecord model, RxModel and so on. Let’s quickly review some of the generated code.

Here’s what our migration looks like.

class CreateContacts < ActiveRecord::Migration
  def self.up
    create_table :contacts do |t|
      t.string :first_name
      t.string :last_name
      t.string :email_address
      t.text :notes
      # For paperclip
      t.column :avatar_file_name, :string
      t.column :avatar_content_type, :string
      t.column :avatar_file_size, :integer
      t.column :avatar_updated_at, :datetime

      t.timestamps
    end
  end

  def self.down
    drop_table :contacts
  end
end

The generator has also added our new Flex view component called ContactBox to the main application file RxModelAttachments.mxml

@@ -16,5 +16,6 @@
   <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:ContactBox/>
   </mx:ViewStack>
 </mx:Application>

And we also got an AS3 model file generated in Contact.as that looks like this.

package rxmodelattachments.models {
  
  import org.restfulx.models.RxModel;
  
  [Resource(name="contacts")]
  [Bindable]
  public class Contact extends RxModel {
    public static const LABEL:String = "firstName";

    public var firstName:String = "";

    public var lastName:String = "";

    public var emailAddress:String = "";

    public var notes:String = "";

    [Ignored]
    public var attachmentUrl:String;
    
    public function Contact() {
      super(LABEL);
    }
  }
}

It pretty much reflects what we had in our model.yml file, but it’s set up to display uploaded files via the attachmentUrl property.

The generator has also added our newly generated RxModel to the list of models for this application in ApplicationController.as

@@ -10,7 +10,7 @@ package rxmodelattachments.controllers {
   public class ApplicationController extends RxApplicationController {
     private static var controller:ApplicationController;
     
-    public static var models:Array = []; /* Models */
+    public static var models:Array = [Contact]; /* Models */
     
     public static var commands:Array = []; /* Commands */
     

And added a RESTful resource to config/routes.rb

@@ -1,4 +1,6 @@
 ActionController::Routing::Routes.draw do |map|
+  map.resources :contacts
+
   
   # Map application root to default RestfulX controller
   map.root :controller => "flex"

The next thing we are going to do is take a peak at the generated Flex view component ContactBox.mxml.

I’ve included the relevant AS3 snippet from it below.

    import org.restfulx.Rx;
    import org.restfulx.utils.RxUtils;
    import rxmodelattachments.models.Contact;
    import org.restfulx.utils.RxFileReference;

    [Bindable]
    private var contact:Contact = new Contact();
    [Bindable]
    private var fileName:String = "None selected";
    
    private var file:RxFileReference;

    private function newContact():void {
      contact = new Contact();
      contactsList.selectedIndex = -1;
    }

    private function saveContact():void {
      updateModelProperties();
      if (contact.id) {
        contact.update({onSuccess: onContactUpdate});
      } else {
        contact.create({onSuccess: onContactCreate});
      }
    }
  
    private function updateModelProperties():void {
      contact.firstName = firstNameTextInput.text;
      contact.lastName = lastNameTextInput.text;
      contact.emailAddress = emailAddressTextInput.text;
      contact.notes = notesTextArea.text;
  
      contact.attachment = file;
    }
   
    private function destroyContact():void {
      contact.destroy({onSuccess: onContactDestroy});
    }
    
    private function onContactSelect():void {
      contact = RxUtils.clone(contactsList.selectedItem) as Contact;
    }
    
    private function onContactCreate(result:Contact):void {
      contact = new Contact;
    }
    
    private function onContactUpdate(result:Contact):void {
      contactsList.selectedItem = result;
      onContactSelect();
    }
    
    private function onContactDestroy(result:Contact):void {
      onContactCreate(result);
    }    

    private function chooseFile():void {
      file = new RxFileReference("avatar");
      file.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler, false, 0, true);
      file.addEventListener(Event.SELECT, selectFile, false, 0, true);
      file.addEventListener(Event.CANCEL, cancelBrowse, false, 0, true);
      file.browse();
    }
    
    private function selectFile(event:Event):void { 
      fileSelected(event)
    }
    
    private function cancelBrowse(event:Event):void {
      file = null;
    }
    
    private function fileSelected(event:Event):void {
      fileName = RxFileReference(event.target).name;
    }
    
    private function ioErrorHandler(event:Event):void {
      fileChooser.errorString = "Failed to selected a file. Please try again.";
    }

As you can see this file is automatically set-up to do CRUD on our Contact model and it can also perform file uploads for that model out of the box. It may not be the best looking Flex component but it’s certainly a fast way to get started.

Lastly, let’s review our generated ActiveRecord model contact.rb

class Contact < ActiveRecord::Base
  # paperclip examples:
  #   http://github.com/thoughtbot/paperclip/tree/master
  has_attached_file :avatar,
    :styles => { :medium => "600x480>", :thumb  => "100x100#" }
    
  def attachment_url
    avatar.url(:original)
  end
end

It’s been configured to use paperclip with attachment name of avatar. attachment_url method allows us to pass the URL back to Flex client.

Let’s run rake db:refresh, followed by rake rx:flex:build.

That’s it! Try it out by starting the server by running ./script/server and navigating to http://localhost:3000. We should have a functional Flex/Rails application that allows us to do CRUD on Contact model and upload files as well.

Adding Camera Handling and Snapshots

Uploading files is cool, but Flex/Flash allow us to do more than that. If you’ve got a WebCam we can use its output to take snapshots and upload them instead of actual files on the file system. This should save people time uploading their pictures. Now they can just take a picture on the spot instead of looking for the right image on their hard-drive.

Adding Snapshots

Let’s create CameraStream.as. This will encapsulate all the operations we have to perform with Flash video files.

Here’s what it looks like.

package rxmodelattachments.utils {
  import flash.media.Camera;
  import flash.media.Video;
  import flash.display.Sprite;
  import flash.display.BitmapData;
  import flash.geom.Matrix;
  import flash.display.Bitmap;
  import flash.system.Security;
  import flash.system.SecurityPanel;
   
  public class CameraStream extends Sprite {
    public static const DEFAULT_CAMERA_FPS:Number = 30;
     
    public var video:Video;
    
    private var camera:Camera;
    private var cameraWidth:Number;
    private var cameraHeight:Number;

    public function CameraStream(w:Number = 320, h:Number = 240) {
      camera = Camera.getCamera();
      cameraWidth = w;
      cameraHeight = h;
      if (camera != null) {
        camera.setMode(cameraWidth, cameraHeight, DEFAULT_CAMERA_FPS);
        video = new Video(w, h);
        video.attachCamera(camera);
        addChild(video); 
      } else {
        Security.showSettings(SecurityPanel.CAMERA)
      }
    }
    
    public function getSnapshotBitmapData():BitmapData {
      var snapshot:BitmapData = new BitmapData(cameraWidth, cameraHeight);
      snapshot.draw(video,new Matrix());
      return snapshot;
    }
    
    public function getSnapshot():Bitmap {
      var bitmap:Bitmap = new Bitmap(getSnapshotBitmapData());
      return bitmap;
    }
  }
} 

The next thing we need to do is a add a component that will allow us to take snapshots and preview them. Here’s one way to do it.

File: TakeSnapshot.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" 
  width="610" height="410" title="Take a picture" 
  close="PopUpManager.removePopUp(this)" initialize="init()" showCloseButton="true">
   <mx:Script>
       <![CDATA[
         import flash.media.Camera;
         import flash.utils.ByteArray;
         
         import mx.core.UIComponent;
         import mx.graphics.codec.PNGEncoder;
         import mx.managers.PopUpManager;

         import rxmodelattachments.utils.CameraStream;
         
         private var cameraStream:CameraStream;

         private var cameraHeight:int = 280;
         
         private var cameraWidth:int = 260;
         
         private var currentSnapshot:ByteArray;
         
         public var handler:Function;
           
         private function init():void {
           cameraStream = new CameraStream(cameraWidth, cameraHeight);
           var ref:UIComponent = new UIComponent();
           camera.removeAllChildren();
           camera.addChild(ref);
           ref.addChild(cameraStream);
         }
         
         private function takeSnapshot():void {
           var uiComponent:UIComponent = new UIComponent();
           uiComponent.width = cameraWidth;
           uiComponent.height = cameraHeight;
           
           var photoData:Bitmap = cameraStream.getSnapshot();
           var photoBitmap:BitmapData = photoData.bitmapData;
           
           uiComponent.addChild(photoData);
           
           preview.removeAllChildren();
           preview.addChild(uiComponent);
           
           var encoder:PNGEncoder = new PNGEncoder();
           currentSnapshot = encoder.encode(photoBitmap);
         }
         
         private function confirmSnapshot():void {
           handler(currentSnapshot);
           PopUpManager.removePopUp(this);
         }
       ]]>
   </mx:Script>
   <mx:Panel width="280" height="350" id="camera" title="Photo Maker" 
    cornerRadius="0" left="10" top="10" bottom="40"/>
   <mx:Panel width="280" height="350" title="Preview" id="preview" 
    cornerRadius="0" right="10" top="10" bottom="40"/>
   <mx:Button label="Take Snapshot" click="takeSnapshot()" 
    bottom="10" width="110" left="10"/>
   <mx:Button label="Done!" click="confirmSnapshot()" 
    right="10" width="110" bottom="10"/>
</mx:TitleWindow>

Finally, let’s modify our generated view component ContactBox.mxml to be able to use our Snapshot component and upload binary data directly instead of going to the filesystem first. Here’s what I’ve changed

That’s all there is to it.

Run rake rx:flex:build to re-compile our Flex application and then start the server by running ./script/server. Then navigate to http://localhost:3000 and take some snapshots!

Get the code

You can get all the code created in this tutorial at rx_model_attachments or by running
git clone git://github.com/dima/rx_model_attachments.git