6.6 Extending Server-Side ActionScript with Java

Server-Side ActionScript is written entirely in Java, and one of the tremendous advantages of SSAS is that you can extend it in Java as well. If your workplace consists of ActionScript programmers who will be assembling server-side methods using SSAS, a few custom Java functions can provide any functionality that SSAS is missing. For example, the JRun 4 implementation of SSAS is missing the CF object, which is required for database queries. The functionality of the CF object can be mimicked in Java and used from within SSAS. Similarly, the file and directory manipulation techniques of ColdFusion, Java, and ASP.NET are missing from SSAS; these too can be added using Java. I'll show a few simple examples of possible extensions to SSAS and then show a simple CF object with a query( ) method that can be used from within JRun 4.

6.6.1 The Principles of Extending SSAS

Server-Side ActionScript uses the Rhino JavaScript parser, which allows you to call Java methods as follows. To invoke a method of a Java class contained within the java package, first use the new operator to create an instance of the Java class:

var myVar = new java.packagename.classname;

Then call methods on the instance of the class as usual:

myVar.methodname(params);

You can also reference classes that are not in the java package by using the Packages prefix:

var myVar = new Packages.myPackagename.myClassname;

As an example of using a class that is not in the java package, consider the StringReverser class from Example 5-6. Simply create a new .asr file named StringReverser.asr with the method reverseString( ) in it, as shown in Example 6-5.

Example 6-5. The StringReverser in SSAS
function reverseString (target) {
  var temp = new Packages.com.oreilly.frdg.StringReverser(target);
  return temp.getReversedString( );
}

If you use the Flash movie created in Example 5-8, JavaExample1.fla, it should give you the same results as the CFC using the same Java class.

When referencing Java classes that aren't in the java package, you have to make sure the classes are in the classpath of the ColdFusion MX or JRun 4 server?not the web directory path where the SSAS file resides.

More information on the Rhino parser and the techniques for accessing Java from SSAS can be found at http://www.mozilla.org/rhino/scriptjava.html.

One of the key benefits to invoking Java inside of SSAS is to take advantage of the numerous classes that are already part of the java and javax packages. The following examples show some of the simple Java classes that can be used.

6.6.1.1 Adding a Sleep( ) function

When creating client/server communication, you often want to add a delay to the processing. This can be done in SSAS using some fancy scripting and the Date object, but you can do it more easily with a simple Java class, shown in Example 6-6.

Example 6-6. The Sleep( ) method of Sleep.asr pauses a script
function Sleep (howManySeconds) {
  var mySleeper = new java.lang.Thread;
  mySleeper.sleep(howManySeconds * 1000);
}

This function allows you to pass a count, in seconds, of how long you want the script to delay. The following code will cause a three-second sleep:

Sleep(3);
6.6.1.2 Getting a directory list from the server

Unlike ColdFusion, SSAS does not support any built-in directory access methods. Again, this can be accomplished rather easily with Java. The remote method getDirectory( ), shown in Example 6-7, returns a directory in an array, with recursive entries for subdirectories. The function can be saved in a file named Directory.asr in the webroot\com\oreilly\frdg directory.

Example 6-7. Retrieving a directory with SSAS
function getDirectory (theDirectory) {
  // Create a file object for the root directory
  var myFile = new java.io.File (theDirectory) ;
  // Get all the files and directories under the directory
  var myFileList = myFile.listFiles( );
  var theList = new Array( );
  for (var i = 0 ; i < myFileList.length ; i ++ ) {
    if (myFileList[i].isDirectory( )) {
      // If it is a directory, create an object containing 
      // the directory name and file list.
      theList.push({directory:myFileList[i].toString( ),
                    files:getDirectory(myFileList[i])});
    } else if (myFileList[i].isFile( )) {
      theList.push(myFileList[i].toString( ))
    }
  }
  return theList;
}

The getDirectory( ) function accepts a directory path as an argument. The path should be in a string format, such as "e:/cfusionmx/wwwroot/com/oreilly". The path can use slashes (/) to maintain compatibility with Unix servers or backslashes (\) for Windows servers. If you use backslashes, you'll also have to escape them with another backslash, such as "e:\\cfusionmx\\wwwroot\\com\\oreilly". The function uses the File class in the java.io package. The listFiles( ) method grabs an array of files in the directory. The list elements can be files or directories. If the current item is a directory, as indicated by isDirectory( ), we create an ActionScript object with a directory property (the directory path) and a files property. The files property is an array created by calling the getDirectory( ) function recursively. This will play an important part in the process on the client once we return the result. If the current item is not a directory, we add it to the current directory's file list. Finally, we return the list to the caller.

Example 6-8 shows the client-side ActionScript for the Directory service. The method uses a Tree object from the Flash UI Components Set 2. We assume the Tree object is named directory_tree and we populate it with the contents of the remote directory.

Example 6-8. Retrieving the contents of a directory on the server with DirectoryList.fla
#include "NetServices.as"

// The directory to list the contents of
var theDirectory = "e:\\cfusionmx\\wwwroot\\com\\oreilly";

var myURL = "http://localhost/flashservices/gateway";
if (initialized == null) {
  initialized = true;
  NetServices.setDefaultGatewayUrl(myURL);
  var my_conn = NetServices.createGatewayConnection( );
  var myService = my_conn.getService("com.oreilly.frdg.Directory");
}

// Call the remote service to retrieve a directory, given the path
myService.getDirectory(new MyResponder( ), theDirectory);

// Set up the Tree control named directory_tree, assumed to exist already
var myRootNode = new FTreeNode(theDirectory).setIsOpen(true);
directory_tree.setRootNode(myRootNode);

// Responder object for the directory list, with private methods
function MyResponder ( ) {
  this.onResult = function (myResult) {
    listDirectory(myResult, myRootNode);
    directory_tree.refresh( );
  };
  this.onStatus = function (myStatus) {
    trace("Error: "+ myStatus.description);
  };
  function listDirectory (myArray, node) {
    // Populate the Tree object using the directory list from the server
    for (var i=0; i< myArray.length; i++) {
      if (myArray[i] instanceof Object) {
        var new_tree_node = new FTreeNode(myArray[i].directory);
        node.addNode(new_tree_node);
        listDirectory(myArray[i].files, new_tree_node);
      } else {
        node.addNode(new FTreeNode(getFile(myArray[i]), getFile(myArray[i])));
      }
    }
  };
  function getFile (filePath) {
    var lastSlash = filePath.lastIndexOf("\\");
    if (lastSlash != -1) filePath = filePath.substring(lastSlash+1);
    return filePath;
  }
}

Example 6-8 uses recursion again?this time on the client. The array of directories and files is returned from the server, so we cycle through the array and test each item. If it is an object, it is a directory, so we create a root node for the tree using the directory property (the path of the directory) and call the listDirectory( ) method recursively. If it is not an object, then it must be a file path, so we get the filename from the path (using another private method?getFile( )) and add a child node to the current root node of the tree. Figure 6-1 shows the Directory service in use.

Figure 6-1. Recursively listing the contents of a directory on the server
figs/frdg_0601.gif

This example demonstrates one of the striking things about using SSAS for remote services?the separation between client and server is almost seamless. You could, for example, add a filtering mechanism to the Directory service to filter filenames based on their file extensions. The functionality could be placed on the client or on the server. In fact, the same code would work in either place, because it is simply ActionScript code.

6.6.1.3 File methods on the server

The preceding section showed how to use Java from within SSAS to access the filesystem. Following are some general utility functions that can be used in SSAS inside of your remote methods.

Object-oriented techniques don't work as well in SSAS as in client-side ActionScript because of the nature of the language. There are no includes, and the remote methods are accessed when needed, so instantiating objects doesn't make as much sense from a programming perspective. For that reason, the file methods shown in this section are shown as individual functions rather than methods of a class.

The moveFile( ) method shown in Example 6-9 moves a file on the server given a source file path and destination file path.

Example 6-9. Moving a file on the server
function moveFile (source, destination) {
  var myFile = new java.io.File(source);
  if (!myFile.exists( ))
    return false;
  return myFile.renameTo(new java.io.File(destination));
}

The renameFile( ) method shown in Example 6-10 renames a file on the server.

Example 6-10. Renaming a file on the server
function renameFile (sourcepath, newFilename) {
  var myFile = new java.io.File(sourcepath);
  if (!myFile.exists( ) || myFile.isDirectory( ))
    return false;
  newFilename = myFile.getParent( ) +  java.io.File.separator + newFilename;
  return myFile.renameTo(new java.io.File(newFilename));
}

The deleteFile( ) method shown in Example 6-11 deletes a file on the server. It deletes an entire directory if passed a directory path instead of a file path.

Example 6-11. Deleting a file or a directory on the server
function deleteFile (filepath) {
  var success = false;
  var theFile = new java.io.File(filepath);
  var f;
  if (!theFile.exists( ))
    return success;
  if (theFile.isDirectory( )) {
    var allFiles = theFile.list( );
    for (var i=0; i < allFiles.length; i++) {
      f = theFile.getAbsolutePath( ) + java.io.File.separator + allFiles[i];
      deleteFile(f);
    }
  } else {
    try {
      success = theFile.delete( );
    } catch(e) { // noop }
  }
  return success;
}

The createDirectory( ) method shown in Example 6-12 creates a directory on the server.

Example 6-12. Creating a directory on the server
function createDirectory (directoryPath) {
  var theDirectory = new java.io.File(directoryPath);
  if (theDirectory.exists( ))
    return true;
  return theDirectory.mkdir( );
}
6.6.1.4 Sending an email with Server-Side ActionScript

SSAS does not contain any methods for working with SMTP servers, so there is no way to send an email from SSAS . . . or is there? Using the javax.mail.* package, you can script a method that sends emails through a SMTP server. The method shown in Example 6-13 can be saved in webroot\com\oreilly\frdg as Email.asr. It will send an email, given the recipient, sender, subject line, and message body.

Example 6-13. Sending an email from Server-Side ActionScript
function send (to, from, subject, message) {
  try {
    var mailobj = Packages.javax.mail;
    var props = new java.util.Properties( );
    // Substitute your SMTP server address here
    props.put("mail.smtp.host","mail.YourServerNameHere.com");
    var mySession = new mailobj.Session.getInstance(props);
    var myMessage = new mailobj.internet.MimeMessage(mySession);
    var myToField = new mailobj.internet.InternetAddress(to);
    var myFromField = new mailobj.internet.InternetAddress(from);
    var recipientType = mailobj.Message.RecipientType.TO;
    myMessage.setFrom(myFromField);
    myMessage.addRecipients(recipientType, myToField);
    myMessage.setSubject(subject);
    myMessage.setText(message);
    mailobj.Transport.send(myMessage);
  } catch (e) {
    throw ("Error in sending email:" + e);
  }
  return true;
}

The first line of the function inside the try block sets an ActionScript variable to the javax.mail package. This technique is not immediately intuitive to the Java programmer, but it is allowed in SSAS. The Java package can be referenced with the mailobj variable thereafter.

The only change you need to make to this script is to supply a SMTP server address in place of "mail.YourServerNameHere.com". This remote method can be used with the simple email interface created in Chapter 5. Example 6-14 shows the client-side ActionScript code for generating the email application's interface. The interface elements required by Example 6-14 are shown in Table 6-4.

Table 6-4. Interface elements used in Example 6-14

Interface Element

Name

Input text field

to_txt

Input text field

from_txt

Input text field

subject_txt

Input text field

body_txt

PushButton

send_pb

MessageBox

status_mb

Example 6-14. The client-side ActionScript code for sendEmailASR.fla
#include "NetServices.as"

var my_conn;         // Connection object
var emailService;    // Service object
var myURL = "http://localhost/flashservices/gateway";
// Message box that displays status messages
status_mb.visible = false;

// Responder for general service methods
function Responder ( ) {}
Responder.prototype.onResult = function (myResults) {
  if (myResults == true) myResults = "Email sent!";
  status_mb._visible = true;
  status_mb.setMessage(myResults);
};

Responder.prototype.onStatus = function (theError) {
  status_mb._visible = true;
  status_mb.setMessage(theError.description);
  System.onStatus = this.onStatus;
};

// Close the message box when OK is clicked
status_mb.setCloseHandler("closeBox");
function closeBox ( ) {
    status_mb.visible = false;
}

// Initialize Flash Remoting
function init ( ) {
  initialized = true;
  NetServices.setDefaultGatewayUrl(myURL);
  my_conn = NetServices.createGatewayConnection( );
  emailService = my_conn.getService("com.oreilly.frdg.Email");
}

init( );

// Send the email when the send_pb button is clicked
send_pb.setClickHandler("send");
function send ( ) {
  var toAddress = to_txt.text;
  var fromAddress = from_txt.text;
  var subject = subject_txt.text;
  var body = body_txt.text;
  // Call the service, using the responder in the first argument
  emailService.send(new Responder( ), toAddress, fromAddress, subject, body);
}
6.6.1.5 Retrieving emails using Server-Side ActionScript

You can enable SSAS to retrieve email from a POP3 server using the methods of the javax.mail package. The code shown in Example 6-15 demonstrates the steps required to access a POP3 server:

  1. Pass your authentication information to a POP3 server.

  2. Retrieve a folder.

  3. Parse the messages in the folder, retrieving message ID numbers, subjects, from lines, and any other information you might need.

  4. Get the content of each email as part of the multipart email.

To do this, I've created an Inbox class that acts as a simple wrapper on the server for the POP3 access. For the sake of simplicity, the Flash interface is done in three parts:

  1. Get the user's login information.

  2. Grab the contents of the inbox and display the headers.

  3. If the user clicks on an individual email, show the body.

First, the Server-Side ActionScript is shown in Example 6-15. The code is explained with inline comments.

Example 6-15. Retrieving email from a POP3 account in SSAS
function Inbox (myHost, myUsername, myPassword) {
  // The Inbox object opens the connection to the POP3 server
  // and provides methods to receive messages and close connections
  var mailobj = Packages.javax.mail;
  var props = new java.util.Properties( );
  var mySession = new mailobj.Session.getInstance(props);
  this.popAccount = mySession.getStore("pop3");
  this.popAccount.connect(myHost, myUsername, myPassword);
  this.folder = this.popAccount.getFolder("INBOX");
  this.folder.open(mailobj.Folder.READ_ONLY);

  // The getMessages( ) method retrieves all messages
  this.getMessages = function ( ) {
    return this.folder.getMessages( )
  };

  // The getMessage( ) method retrieves one message given a message number
  this.getMessage = function(messageNumber) {
    return this.folder.getMessage(messageNumber);
  };
  // The close( ) method simply closes connections to the POP3 server
  this.close = function ( ) {
    this.folder.close(false);
    this.popAccount.close( );
  };
}

// retrieveMessages( ) retrieves a list of headers given three arguments:
// myHost (POP3 account), myUsername (login name), myPassword (password)
function retrieveMessages (myHost, myUsername, myPassword) {
  var myInbox = new Inbox(myHost, myUsername, myPassword);
  var myMessages = myInbox.getMessages( );
  // The raw headers can't be sent via Flash Remoting, 
  // so we serialize them manually
  var serializedHeaders = serializeHeaders(myMessages);
  // Close the connection to the inbox
  myInbox.close( );
  return serializedHeaders;
}

// retrieveMessage( ) retrieves one message given four arguments:
// myHost (POP3 account), myUsername (login name), myPassword (password),
// and the message number.
function retrieveMessage (myHost, myUsername, myPassword, messageNumber) {
  var myInbox = new Inbox(myHost, myUsername, myPassword);
  var myMessage = myInbox.getMessage(messageNumber);
  // The raw message can't be sent via Flash Remoting,
  // so we serialize it manually
  var serializedMessage = serializeMessage(myMessage);
  // Close the connection to the inbox
  myInbox.close( );
  return serializedMessage;
}

// serializeHeaders( ) takes a messages array and extracts/serializes
// the header information (from, subject, messagenumber)
function serializeHeaders (messages) {
  var serializedHeaders = new Array( );
  var header;
  for (var i=0; i < messages.length; i++) {
    // Call our own general-purpose header serialization routine
    header = serializeHeader(messages[i]);
    serializedHeaders.push(header);
  }
  return serializedHeaders;
}

// serializeHeader( ) takes one message argument and extracts header information
function serializeHeader (message) {
  var header = new Object( );
  header.messageNumber = message.getMessageNumber( );
  header.from = message.getFrom( );
  header.subject = message.getSubject( );
  return header;
}

// serializeMessage( ) takes a message as an argument and extracts only
// the text portion of the message. The rest of the parts are simply
// counted as attachments. You can enhance this function to return other
// parts of messages as well.
function serializeMessage (message) {
  var serializedMessage = serializeHeader(message);
  serializedMessage.attachments = 0;
  var tempPart;
  if (message.isMimeType("multipart/*")) {
    var content = message.getContent( );
    for (var i=0; i<content.getCount( ); i++) {
      tempPart = content.getBodyPart(i);
      if (tempPart.isMimeType("text/plain")) {
        serializedMessage.text = tempPart.getContent( );
      } else {
        serializedMessage.attachments++;
      }
    }
  } else if (message.isMimeType("text/plain")) {
      serializedMessage.text = message.getContent( );
  } else {
      serializedMessage.attachments++;
  }
  return serializedMessage;
}

Next, we must write the client-side ActionScript. This interface will have three "pages"?login, headers, and message. Because this is a simple demonstration, the message is retrieved from the server when the user wants to read it. In practice, you would probably set up a class to handle the messages on the client side and save the messages into a local SharedObject. The commented client-side code is shown in Example 6-16.

Example 6-16. Client-side ActionScript code for email retrieval
#include "NetServices.as"

// Set up the components
status_mb._visible = false;
grid_lb._visible = false;
message_pb.setClickHandler("messageClicked");
login_pb.setClickHandler("loginClicked");
message_pb._visible = false;

// Set up global vars
var login;
var password;
var popServer;

// Set up the gateway URL and initialization function
var myURL = "http://localhost/flashservices/gateway";
function init ( ) {
  NetServices.setDefaultGatewayUrl(myURL);
  var my_conn = NetServices.createGatewayConnection( );
  // Set up the Email service
  var myService = my_conn.getService("flashremoting.com.oreilly.frdg.Email");
}
init( );

// Click handler for Login button:
// Get the message headers
function loginClicked ( ) {
  status_mb._visible = true;
  status_mb.setButtons( );
  login = login_txt.text;
  password = password_txt.text;
  popserver = popserver_txt.text;
  myService.retrieveMessages(new HeaderResponder( ), popserver, login, password);
  gotoAndPlay("login");
}

// Display the headers
function headersClicked ( ) {
  grid_lb._visible = true;
  gotoAndPlay("headers");
}

// Get the current message
function messageClicked( )  {
  var message = grid_lb.getSelectedItem( ).data;
  if (message)
    myService.retrieveMessage(new MessageResponder( ), 
                              popserver, login, password, message);
}

// Responder to grab all headers and display in list box
function HeaderResponder ( ) {}

HeaderResponder.prototype.onResult = function(myResults) {
  status_mb._visible = false;
  grid_lb._visible = true;
  for (var i in myResults)
    grid_lb.addItem(myResults[i].subject,myResults[i].messageNumber);
  message_pb._visible = true;
  gotoAndPlay("headers");
};

HeaderResponder.prototype.onStatus = function (theError) {
  trace(theError.description);
};

// Responder to grab messages and display in message page
function MessageResponder ( ) {}

MessageResponder.prototype.onResult = function (myResults) {
  status_mb._visible = false;
  grid_dg._visible = false;
  gotoAndPlay("message");
  subject_txt.text = myResults.subject;
  from_txt.text = myResults.from + " <" + myResults.address + ">";
  date_txt.text = myResults.date;
  body_txt.text = myResults.body;
};

MessageResponder.prototype.onStatus = function (theError) {
  trace(theError.description);
};

stop( );

The client-side code uses a ListBox component rather than a DataGrid, since the ListBox component is preinstalled with Flash and freely available. You could just as easily use a DataGrid for your own implementation.

The two service calls?retrieveMessages( ) and retrieveMessage( )?each use their own responder object. When retrieving multiple messages, only the headers are retrieved, which are placed in the ListBox. When retrieving one message, the body of the message is also retrieved.

6.6.2 Creating a CF.query( ) Method for JRun 4

ColdFusion MX users have access to databases from SSAS using the CF.query( ) method. JRun 4 users have no way to access databases from within SSAS unless they know Java and know how to extend SSAS in Java. At the time of this writing, there is no way to return a resultset from a Java application to a Flash movie, as you would do with ColdFusion or ASP.NET. Resultsets have to be manually parsed on the server and placed into arrays or CachedResultSets. (See Chapter 7 for more details on the Java implementation of Flash Remoting.)

The code shown in Example 6-17 partially emulates the CF.query( ) method from within your JRun 4 SSAS files, but it returns the result as an array of objects that you can parse manually in the Flash movie. The CF.query( ) method for JRun uses data sources that are defined in the JRun administrative interface and takes two arguments: datasource and sql.

Example 6-17. The CF object created for a JRun SSAS implementation
CF = new Object( );
CF.query = function (datasource, sql) {
  // InitialContext for JRun data source names
  var ctx = new Packages.javax.naming.InitialContext( );
  // Find the data source
  var ds = ctx.lookup(datasource);
  var dbConnection = ds.getConnection( );
  var stmt = dbConnection.prepareStatement(sql);
  if (sql.match(/^select\s*/i)) {
    var rs = stmt.executeQuery( );
    var rsmd = rs.getMetaData( );
    var myRecordSet = new Object( );
    myRecordSet.columnNames = getColumnNames(rsmd);
    rs_hasData = rs.next( );
    if (rs_hasData) {
      myRecordSet.items = serializeData(rs, myRecordSet.columnNames);
    }
    myRecordSet.totalCount = (rs_hasData) ? myRecordSet.items.length : 0;
    rs.close( );
  } else {
    return stmt.executeUpdate( );
  }
  stmt.close( );
  dbConnection.close( );
  return myRecordSet;
};

// Get the column names of the resultset
function getColumnNames (metadata) {
  var columns = new Array( );
  for (var i=1; i<= metadata.getColumnCount( ); i++)
    columns.push(metadata.getColumnLabel(i));
  return columns;
}

// Serialize the data for returning to Flash
function serializeData (rs, columns) {
  var rs_hasData = true;
  // rows holds the rows
  var rows = new Array( );
  // currentRow will hold individual row
  var currentRow = new Object( );
  // z is a mapping -- integer indexes that match column names
  var z = new Array( );
  var columnCount = columns.length;
  // Get index mapping of column names
  for (var i = 0; i < columnCount; i++)
    z.push(rs.findColumn(columns[i]));
  while (rs_hasData) {
    for (i = 0; i < columnCount; i++) {
      currentRow[columns[i]] = (rs.getObject(z[i]));
    }
    // Add to our permanent recordset
    rows.push(currentRow);
    // Clear the row out again
    currentRow = new Object( );
    rs_hasData = rs.next( );
  }
  return rows;
}

Simply add the code shown in Example 6-17 to an SSAS file, and you will be able to access CF.query( ) functionality from a JRun 4 SSAS file. The data is returned as an array of row objects, but a little bit of client-side ActionScript code added to your Flash movie converts it into a RecordSet object:

JRunRecordset.prototype = new Recordset( );
function JRunRecordset (rs) {
  super (rs.columnNames);       // Call the RecordSet constructor
  for (i in rs.items)
    this.addItem(rs.items[i]);  // Add records to RecordSet
}

Now you can convert the returned array of objects into an ActionScript RecordSet object by instantiating a new JRunRecordset object. Here is the updated responder object for the ProductsAdmin.fla file from Example 5-14 (changes are shown in bold):

SearchResult.onResult = function (result_rs) {
  Products_rs = new JRunRecordset(result_rs);
  results_txt.text = "There were " + Products_rs.getLength( )+ 
   " records returned.";
  Products_rs.move("First");
  getRecord( );
};

Example 5-14 also contains two RecordSet objects that feed data to the drop-down ComboBoxes. These ComboBoxes are created in the ComboBoxResponder object. Change the ComboBoxResponder.onResult( ) method from Example 5-14 to add the JRunRecordset (changes are shown in bold):

function ComboBoxResponder (cbName) {
  this.cbName = cbname;
}
ComboBoxResponder.prototype.onResult = function (result_rs) {
  result_rs = new JRunRecordset(result_rs);
  var fields = result_rs.getColumnNames( );
  var idField = '#' + fields[0] + '#';
  var descField = '#' + fields[1] + '#';
  DataGlue.bindFormatStrings(this.cbName, result_rs, descField,idField);
};

The custom CF.query( ) method for JRun 4 will work with most of the online examples that you'll find for Server-Side ActionScript at the Macromedia web site and other places.



    Part III: Advanced Flash Remoting