Pages

Friday, August 29, 2008

Flex and Ruby on Rails with RubyAMF

My earlier Flex blogs often talks about Flex in combination with java but you don't need to java you can use Ruby on Rails too. There are a lot blog entries how to do this. I already made a little demo application where I use RoR and the HttpService in Flex. This solution is based on REST Web Services. RoR gives xml as output.
In this Blog entry I will use RubyAMF. With RubyAMF I can use RemoteObject, RubyAMF will convert the Ruby datatypes to the Flex datatypes and it is really fast.
If you want more information on RubyAMF just click here or go to the RubyAMF discussion groups

Let's create a new Flex Ruby application where we will use RubyAMF. I will create a small comic entry application.

Create a new RoR application:
rails comics_catalog

Change directory to the new comics_catalog folder:
cd comics_catalog

Edit the database connection file ( comics_catalog\config\database.yml ) with the right connection.

Update the project with RubyAMF:
ruby script/plugin install http://rubyamf.googlecode.com/svn/tags/current/rubyamf
This will download RubyAMF code and configuration from subversion to your project.

Create the comic table and controller
ruby script/generate rubyamf_scaffold comic

Edit the comic table configuration. Go to comics_catalog\db\migrate\ folder and edit the 20080827214400_create_comics.rb file. You will have an other filename because of the timestamp. The script need to look like this.

class CreateComics < ActiveRecord::Migration
def self.up
create_table :comics do |t|
t.string :name
t.string :description
t.float :price
t.timestamps
end
end

def self.down
drop_table :comics
end
end

Let's create the comic table in the database
rake db:migrate

The last step in RoR is to configure RubyAMF. For this we need to go the comics_catalog\config folder where we will edit the rubyamf_config.rb file.

require 'app/configuration'
module RubyAMF
module Configuration
ClassMappings.ignore_fields = ['created_at','created_on','updated_at','updated_on']
ClassMappings.translate_case = true
ClassMappings.assume_types = false
ParameterMappings.scaffolding = false


ClassMappings.register(:actionscript => 'Comic',
:ruby => 'Comic',
:type => 'active_record',
:attributes => ["id","name","description","price", "created_at", "updated_at"])

ClassMappings.force_active_record_ids = true
ClassMappings.use_ruby_date_time = false
ClassMappings.use_array_collection = false
ClassMappings.check_for_associations = false
ParameterMappings.always_add_to_params = true
end
end

I use ClassMappings.ignore_fields because I want to ignore the automatic created fields Ruby will fill these field automatically.
ClassMappings.translate_case is important if you want to Java naming style. RubyAMF will convert created_at to createdAt.
ClassMappings.assume_types is important if you do your know class mapping. In our case we will create the comic class in Flex too so we don't need this. This will give us a little more performance.
ParameterMappings.scaffolding is handy if you just want to pass an comic object or id to the delete method else you need to use {id:dg.selecteditem.id}
ClassMappings.register(:actionscript => 'Comic'. Don't use the package name just put in the actionscript class name.

Start the Ruby server
ruby script/server

Now we can go to Flex, where we create an new application.

first create a comic actionscript object

package vo
{

[RemoteClass(alias="Comic")]
[Bindable]
public class Comic
{
public var id:int;
public var name:String;
public var description:String;
public var price:Number;
public var createdAt:Date;
public var updatedAt:Date;
public function Comic()
{
}


}
}

And here is the mxml code.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="loadAll2();">

<mx:Script>
<![CDATA[
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.controls.Alert;
import vo.Comic;

[Bindable]
private var comics:Array = new Array();

private function loadAll2():void {
var token:AsyncToken = AsyncToken(comicService.find_all());
token.kind = "fill";
}

private function save2():void {
var comic:Comic = new Comic();
comic.id = dg2.selectedItem.id;
comic.name = Name2.text;
comic.description = Description2.text;
var token:AsyncToken = AsyncToken(comicService.save(comic));
token.kind = "save";

}

private function create2():void {
var comic:Comic = new Comic();
comic.name = Name2.text;
comic.description = Description2.text;
var token:AsyncToken = AsyncToken(comicService.save(comic));
token.kind = "create";
}


private function destroy2():void {
var token:AsyncToken = AsyncToken(comicService.destroy(dg2.selectedItem.id));
token.kind = "delete";
}

private function faultHandler(event:FaultEvent):void {
Alert.show(event.fault.faultString + " : " + event.fault.faultCode + " : " + event.fault.faultDetail , "Error in LoginCommand");
}

private function resultHandler2(event:ResultEvent):void {
if ( event.token.kind == "fill" ) {
comics = event.result as Array;
} else {
loadAll2();
}
}



]]>
</mx:Script>
<mx:RemoteObject id="comicService" destination="rubyamf"
endpoint="http://localhost:3000/rubyamf_gateway/"
source="ComicsController"
showBusyCursor="true"
result="resultHandler2(event)"
fault="faultHandler(event)" />

<mx:ApplicationControlBar>
<mx:Button label="Create" click="create2()"/>
<mx:Button label="Update" click="save2()"/>
<mx:Button label="Delete" click="destroy2()"/>
<mx:Button label="Refresh" click="loadAll2()"/>
</mx:ApplicationControlBar>

<mx:DataGrid id="dg2" dataProvider="{comics}">
<mx:columns>
<mx:DataGridColumn dataField="id" headerText="Key"/>
<mx:DataGridColumn dataField="name" headerText="Name"/>
<mx:DataGridColumn dataField="description" headerText="Description"/>
</mx:columns>
</mx:DataGrid>
<mx:Form >
<mx:FormItem label="Name">
<mx:TextInput id="Name2" text="{dg2.selectedItem.name}" />
</mx:FormItem>
<mx:FormItem label="Description">
<mx:TextInput id="Description2" text="{dg2.selectedItem.description}"/>
</mx:FormItem>
</mx:Form>
</mx:Application>


That's All

Sunday, August 24, 2008

Flex upload and download with Blazeds

With Actionscript 3 and Flashplayer 10 you can now use the new FileReference features. For example you download and upload a file with RemoteObjects ( Blazeds / LifeCycle ). In the early days you had to use a upload and download servlets.
To use the new FileReference features please read these two articles flexexamples.com and using flashplayer 10

Here are some screenshots of the flex application.
First we can upload a file. The datagrid show the status of the uploaded files.

Off course we can download some files from a remote server. First we need to get a list of the remote files. For this we need to press the Get remote files button.

We can select a file and press the Retrieve File Button. When the status is ready we can save this file by pressing the Save File Button.

Here you can download the Flex source code

The code of the upload panel

<?xml version="1.0" encoding="utf-8"?>
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical" width="100%" height="100%"
title="Upload Files">


<mx:Script>
<![CDATA[
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;

private var refUploadFile:FileReference;

private var UploadFiles:Array = new Array();


// Called to add file(s) for upload
private function addFiles():void {
refUploadFile = new FileReference();
refUploadFile.browse();
refUploadFile.addEventListener(Event.SELECT,onFileSelect);
refUploadFile.addEventListener(Event.COMPLETE,onFileComplete);
}

// Called when a file is selected
private function onFileSelect(event:Event):void {
UploadFiles.push({ name:refUploadFile.name,
size:formatFileSize(refUploadFile.size),
status:"initial"});
listFiles.dataProvider = UploadFiles;
listFiles.selectedIndex = UploadFiles.length - 1;

refUploadFile.load();
for ( var i:int = 0 ; i < UploadFiles.length ; i++ ) {
if( UploadFiles[i].name == refUploadFile ) {
UploadFiles[i].status = "loaded";
listFiles.dataProvider = UploadFiles;
break;
}
}
}

// Called to format number to file size
private function formatFileSize(numSize:Number):String {
var strReturn:String;
numSize = Number(numSize / 1000);
strReturn = String(numSize.toFixed(1) + " KB");
if (numSize > 1000) {
numSize = numSize / 1000;
strReturn = String(numSize.toFixed(1) + " MB");
if (numSize > 1000) {
numSize = numSize / 1000;
strReturn = String(numSize.toFixed(1) + " GB");
}
}
return strReturn;
}

private function onFileComplete(event:Event):void
{
refUploadFile = event.currentTarget as FileReference;
var data:ByteArray = new ByteArray();
refUploadFile.data.readBytes(data,0,refUploadFile.data.length);

var token:AsyncToken = AsyncToken(
remoteUpload.doUpload(data, refUploadFile.name)
);

token.kind = refUploadFile.name;

for ( var i:int = 0 ; i < UploadFiles.length ; i++ ) {
if( UploadFiles[i].name == refUploadFile ) {
UploadFiles[i].status = "upload";
listFiles.dataProvider = UploadFiles;
break;
}
}
}

private function uploadResultHandler(event:ResultEvent):void
{
for ( var i:int = 0 ; i < UploadFiles.length ; i++ ) {
if( UploadFiles[i].name == event.token.kind ) {
UploadFiles[i].status = "finished";
listFiles.dataProvider = UploadFiles;
break;
}
}
}

private function faultResultHandler(event:FaultEvent):void
{
for ( var i:int = 0 ; i < UploadFiles.length ; i++ ) {
if( UploadFiles[i].name == event.token.kind ) {
UploadFiles[i].status = "error";
listFiles.dataProvider = UploadFiles;
break;
}
}
}


]]>
</mx:Script>

<mx:RemoteObject id="remoteUpload" destination="FileUtils"
result="uploadResultHandler(event)"
fault="faultResultHandler(event)"/>


<mx:Canvas width="100%" height="100%">
<mx:DataGrid id="listFiles" left="0" top="0" bottom="0" right="0"
allowMultipleSelection="true" verticalScrollPolicy="on"
draggableColumns="false" resizableColumns="false" sortableColumns="false">
<mx:columns>
<mx:DataGridColumn headerText="File" width="150" dataField="name" wordWrap="true"/>
<mx:DataGridColumn headerText="Size" width="50" dataField="size" textAlign="right"/>
<mx:DataGridColumn headerText="Status" width="50" dataField="status" textAlign="right"/>
</mx:columns>
</mx:DataGrid>
</mx:Canvas>
<mx:ControlBar horizontalAlign="center" verticalAlign="middle">
<mx:Button id="btnAdd" toolTip="Add file(s)" click="addFiles()"
label="Upload Files" width="150"/>
</mx:ControlBar>
</mx:Panel>


The code of the download panel

<?xml version="1.0" encoding="utf-8"?>
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
width="100%" height="100%" title="Download Files">

<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.AsyncToken;

private var UploadFiles:Array = new Array();
private var UploadFilesColl:ArrayCollection = new ArrayCollection();
private var fileData:ByteArray = new ByteArray();
private var fileName:String;


private function uploadResultHandler(event:ResultEvent):void
{
if ( event.token.kind == "remoteFileList") {
UploadFilesColl = event.result as ArrayCollection;
for ( var i:int = 0 ; i < UploadFilesColl.length ; i++ ) {
UploadFiles.push({ name:UploadFilesColl[i]
, status:"initial"});
}
listFiles.dataProvider = UploadFiles;
} else {
fileData = event.result as ByteArray;
fileName = event.token.kind;
for ( var b:int = 0 ; b < UploadFiles.length ; b++ ) {
if( UploadFiles[b].name == event.token.kind ) {
UploadFiles[b].status = "Ready";
listFiles.dataProvider = UploadFiles;
break;
}
}

}
}

private function faultResultHandler(event:FaultEvent):void
{
}

private function saveFile(event:Event):void
{
var fileReference:FileReference = new FileReference();
fileReference.save(fileData,fileName);
}

private function getRemoteFiles(event:Event):void
{
var token:AsyncToken = AsyncToken(remoteDownload.getDownloadList());
token.kind = "remoteFileList";
}

private function getDownload(event:Event):void
{
var token:AsyncToken = AsyncToken(
remoteDownload.doDownload(listFiles.selectedItem.name));
token.kind = listFiles.selectedItem.name;
}
]]>
</mx:Script>


<mx:RemoteObject id="remoteDownload" destination="FileUtils"
result="uploadResultHandler(event)"
fault="faultResultHandler(event)"/>


<mx:Canvas width="100%" height="100%">
<mx:DataGrid id="listFiles" left="0" top="0" bottom="0" right="0"
verticalScrollPolicy="on"
draggableColumns="false" resizableColumns="false" sortableColumns="false">
<mx:columns>
<mx:DataGridColumn headerText="File" width="150" dataField="name" wordWrap="true"/>
<mx:DataGridColumn headerText="Status" width="50" dataField="status" textAlign="right"/>
</mx:columns>

</mx:DataGrid>
</mx:Canvas>
<mx:ControlBar horizontalAlign="center" verticalAlign="middle">
<mx:Button id="btnList" toolTip="List remote files"
width="150"
label="Get Remote Files"
click="getRemoteFiles(event)"/>
<mx:Button id="btnRetrieve" toolTip="Retrieve file"
width="150" click="getDownload(event)" label="Retrieve File"/>
<mx:Button id="btnSave" toolTip="Save file"
width="150" click="saveFile(event)" label="Save File"/>
</mx:ControlBar>



</mx:Panel>

The java code

package nl.ordina.flex;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import java.util.ArrayList;
import java.util.List;

public class FileUtils {


public String doUpload(byte[] bytes, String fileName) throws Exception
{
fileName = System.getProperty("java.io.tmpdir") + "/" + fileName;
File f = new File(fileName);
FileOutputStream fos = new FileOutputStream(f);
fos.write(bytes);
fos.close();
return "success";
}

public List getDownloadList()
{
File dir = new File(System.getProperty('java.io.tmpdir'));
String[] children = dir.list();
List dirList = new ArrayList();
if (children == null) {
// Either dir does not exist or is not a directory
} else {
for (int i=0; i<children.length; i++) {
// Get filename of file or directory
dirList.add( children[i]);
}
}
return dirList;
}

public byte[] doDownload(String fileName)
{
FileInputStream fis;
byte[] data =null;
FileChannel fc;

try {
fis = new FileInputStream(System.getProperty("java.io.tmpdir") + "/" + fileName);
fc = fis.getChannel();
data = new byte[(int)(fc.size())];
ByteBuffer bb = ByteBuffer.wrap(data);
fc.read(bb);
} catch (FileNotFoundException e) {
// TODO
} catch (IOException e) {
// TODO
}
return data;
}
}

Sunday, August 17, 2008

Cairngorm Flex MVC is now opensource

Cairngorm is MVC framework which you can use in Adobe Flex or Adobe AIR. In Flex you can put all your code in one mxml but this makes hard to develop and maintain when the project has a particular size. Adobe has developed Cairngorm for it's own projects and it is very positif they released it to the opensource community. Adobe already released BlazeDS, so what's next?
You can download Cairngorm from here.
Here is a cairngorm example application which gives you a nice overview what Cairngorm does. This is a login application. Use admin as username and password.