Exchanging Complex Data with Back-End Systems
Christophe Coenraets
Adobe
Retrieving server data in a Macromedia Flex application is generally easy and demonstrated in many sample applications. Sending complex data back to the server can be slightly more involved, however.
Depending on your role, and on the structure of your development team, you may have the luxury to choose—or at least influence—the architecture used to exchange data between the client and the server: web services, XML over HTTP, or remote method invocation.
In a service-oriented architecture, however, it is also likely that your application will have to access services that you didn't build and over which you have no control. In that case, you have to use the data exchange architecture implemented by the service provider.
This article takes a close look at different strategies to send complex data to the server using the three data services available in Flex to accommodate the heterogeneous data access requirements of your application: HTTPService, RemoteObject, and WebService.
Requirements
To complete this tutorial you will need to install the following software and files:
Macromedia Flex
Macromedia Flash Player
A text editor or Macromedia Flex Builder
Tutorials and sample files:
- complexdata.zip (ZIP, 23K)
Prerequisite knowledge:
Basic experience with the Flex programming model and application framework.
Using HTTPService
There are two commonly used approaches to send data to the server using the HTTPService:
- Passing request parameters
- Sending XML over HTTP
Passing Request Parameters
The request-parameters approach works fine for simple data. In the Employee Directory application (see Figure 1), we send basic employee information (first name, last name, salary, and start date) to a JavaServer page.
Figure 1. Simple Employee Directory application
Here is the source code for the simple Employee Directory application:
employeehttp.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.macromedia.com/2003/mxml" backgroundColor="#FFFFFF"> <mx:HTTPService id="srv" url="employeehttp.jsp" method="POST"> <mx:request> <firstName>{firstName.text}</firstName> <lastName>{lastName.text}</lastName> <salary>{salary.text}</salary> <startDate>{startDate.text}</startDate> </mx:request> </mx:HTTPService> <mx:Form> <mx:FormItem label="First Name"> <mx:TextInput id="firstName"/> </mx:FormItem> <mx:FormItem label="Last Name"> <mx:TextInput id="lastName"/> </mx:FormItem> <mx:FormItem label="Salary"> <mx:TextInput id="salary"/> </mx:FormItem> <mx:FormItem label="Start Date"> <mx:DateField id="startDate"/> </mx:FormItem> <mx:FormItem> <mx:Button label="Add Employee" click="srv.send()"/> </mx:FormItem> </mx:Form> </mx:Application>
At the server side, it's business as usual: You access the request parameters using the traditional API provided by your server-side language. Here is a JSP example:
employeehttp.jsp
<% String firstName= request.getParameter("firstName"); String lastName= request.getParameter("lastName"); String salary= request.getParameter("salary"); String startDate= request.getParameter("startDate"); // Some business logic to process the data %> <status>ok</status>
One advantage of this approach is that, from the server point of view, it works exactly like an HTML front end. This makes it particularly easy to replace an HTML-based user interface with a Flex front end.
In HTML, the request parameters approach works fine, mainly because the client side of the application is stateless and because the data-entry process is split across multiple pages. Therefore the data submitted to the server in one single request is never overly complex.
By contrast, a Flex application is stateful; the data-entry process is not constrained by the page-centric nature of HTML. You use rich user interface components to allow users to enter more complex data in a single integrated user interface. For example, Figure 2 represents an improved version of the Employee Directory application in Figure 1, where users can enter an unlimited number of phone numbers for an employee.
Figure 2. Improved Employee Directory application
With the availability of more sophisticated user interfaces, you will often need to submit more complex data to the server in one single request than you do in HTML. When the complexity of the data increases, the request-parameters approach becomes cumbersome. In that case, sending an XML document is usually a better approach.
Sending XML Over HTTP
There are two major approaches to sending XML data over HTTP:
- Sending XML as a request parameter
- Sending XML in an "application/xml" request
Sending XML as a Request Parameter: The code below provides an implementation of the Employee Directory application, where data is sent to the server in an XML string passed as a request parameter:
employeexml.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.macromedia.com/2003/mxml" backgroundColor="#FFFFFF" initialize="initApp()"> <mx:Script> var employee: Employee; function initApp() { employee=new Employee(); employee.phoneNumbers=new Array(); } function addEmployee() { employee.firstName=firstName.text; employee.lastName=lastName.text; employee.startDate=startDate.selectedDate; employee.salary=Number(salary.text); srv.send({xml: XMLUtil.as2xml(employee, "employee")}); } </mx:Script> <mx:HTTPService id="srv" url="employeexml.jsp" method="POST"/> <mx:Form> <mx:FormItem label="First Name"> <mx:TextInput id="firstName"/> </mx:FormItem> <mx:FormItem label="Last Name"> <mx:TextInput id="lastName"/> </mx:FormItem> <mx:FormItem label="Salary"> <mx:TextInput id="salary"/> </mx:FormItem> <mx:FormItem label="Start Date"> <mx:DateField id="startDate"/> </mx:FormItem> <mx:FormItem label="Phone Numbers" verticalGap="1"> <mx:DataGrid id="dg" editable="true" dataProvider="{employee.phoneNumbers}"> <mx:columns> <mx:Array> <mx:DataGridColumn columnName="type" headerText="Type" cellRenderer="PhoneTypeRenderer"/> <mx:DataGridColumn columnName="number" headerText="Number"/> </mx:Array> </mx:columns> </mx:DataGrid> <mx:HBox horizontalGap="1" width="100%" horizontalAlign="right"> <mx:Button label="+" cornerRadius="0" borderThickness="0" width="20" height="20" click="dg.addItem(new PhoneNumber('H'))"/> <mx:Button label="-" cornerRadius="0" borderThickness="0" width="20" height="20" click="dg.removeItemAt(dg.selectedIndex)" /> </mx:HBox> </mx:FormItem> <mx:FormItem> <mx:Button label="Add Employee" click="addEmployee()"/> </mx:FormItem> </mx:Form> </mx:Application>
Notice that the application uses a simple object-to-XML conversion utility. Here is its source code:
XMLUtil.as
class XMLUtil { static function as2xml(obj: Object, nodeName: String) { var xml:XML=new XML(); xml.appendChild(xml.createElement(nodeName)); for (var i in obj) { handleItem(xml, obj[i], i); } return xml; } private static function handleItem(xml, item, nodeName: String) { var type=typeof item; if (type=="string" || type=="number" || item instanceof Date) { var el=xml.createElement(nodeName); el.appendChild(xml.createTextNode(item)); xml.firstChild.appendChild(el); } else if (item instanceof Array) { for(var i=0; i<item.length; i++) handleItem(xml, item[i], nodeName); } else if (item instanceof Object) xml.firstChild.appendChild(as2xml(item, nodeName)); } }
The Employee.as and PhoneNumber.as classes used in the application are defined as follows:
class Employee { public var firstName : String; public var lastName : String; public var salary : Number; public var startDate : Date; public var phoneNumbers : Array; } class PhoneNumber { public var number : String; public var type : String; function PhoneNumber(type: String) { this.type=type; } }
In this example, the benefits of first loading the data in an ActionScript object (Employee.as) before creating the XML document might not be obvious. However, in Flex applications, you are typically manipulating data as objects. (Even when you retrieve an XML document from the server using the HTTPService, the data is deserialized into an object graph by default.) So when you use the HTTPService, the need to convert objects to XML is a very common scenario.
At the server side, you access the "xml" request parameter using the API provided by the language you are using. You then typically use the XML parsing API provided by that language to process the information further. Here is a JSP example:
employeexml.jsp
<% String xml=request.getParameter("xml"); // Some business logic to process the data %> <status>ok</status>
Sending XML in an "application/xml" Request: To use this approach, just add contentType="application/xml"
to the HTTPService tag definition in employeexml.mxml, and replace the following:
srv.send({xml: XMLUtil.as2xml(employee, "employee")});
with this:
srv.send(XMLUtil.as2xml(employee, "employee"));
At the server side, your application now needs to handle an "application/xml" request. To do so, you typically use an input stream to read the incoming request. You then parse the received XML document using the parsing API provided by your server-side language. Here is a JSP example:
<%@ page import="java.io.BufferedReader, javax.xml.parsers.DocumentBuilderFactory, javax.xml.parsers.DocumentBuilder, org.w3c.dom.Document, org.xml.sax.InputSource"%> <% BufferedReader br = new BufferedReader(request.getReader()); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder db = factory.newDocumentBuilder(); Document doc = db.parse(new InputSource(br)); // Process the XML document %> <status>ok</status>
One nice feature of Flex when you use "application/xml" requests is that you actually don't have to perform the object-to-XML transformation yourself. You can provide an object as the request content and let Flex convert it automatically to XML.
To use the automatic object-to-XML encoding in the Employee Directory application, replace the following:
srv.send(XMLUtil.as2xml(employee, "employee"));
with this:
srv.send(employee);
Flex also allows you to substitute the default encoding logic with your own logic by providing an implementation for the xmlEncode()
method of the HTTPService. For example, to substitute the default encoding logic with the logic implemented in XMLUtil.as, add the following block of code to the initApp()
method:
srv.xmlEncode=function(obj: Object) { return XMLUtil.as2xml(obj, "data"); }
Using RemoteObject
The RemoteObject service allows you to invoke methods remotely in Java objects deployed in your application server. Flex.NET, currently in beta, will allow you to remotely invoke methods in .NET objects.
When invoking methods remotely, you can pass objects back and forth (as the methods' input parameters and return value) between the client and the server.
The RemoteObject approach makes particular sense when both the front end and the back end of your application manipulate data as objects. In this case, the intermediate step of converting objects to XML may result in unnecessary overhead.
When you use RemoteObject, Flex takes care of the serialization/deserialization process. The RemoteObject uses AMF (Action Message Format) over HTTP. AMF is a binary encoding format that makes this solution faster and leaner in terms of bandwidth usage than the other solutions, especially when manipulating large data sets.
In the RemoteObject implementation of the Employee Directory application, we use a Java class named EmployeeService. The EmployeeService class is defined as follows:
package samples.data; public class EmployeeService { public void addEmployee(EmployeeVO employee) { // Output data to console for demo purpose System.out.println("First Name: "+employee.getFirstName()); System.out.println("Last Name: "+employee.getLastName()); System.out.println("Salary: "+employee.getSalary()); System.out.println("Start Date: "+employee.getStartDate()); System.out.println("Phone Numbers:"); PhoneNumberVO[] phoneNumbers=employee.getPhoneNumbers(); PhoneNumberVO phoneNumber=null; for (int i=0; i<phoneNumbers.length; i++) { phoneNumber = phoneNumbers[i]; System.out.println(phoneNumber.getType()+": "+phoneNumber.getNumber()); } // Some business logic to process the data } }
The addEmployee()
method takes an EmployeeVO object as an argument. The EmployeeVO class is defined as follows:
package samples.data; import java.util.Date; public class EmployeeVO { private String firstName; private String lastName; private int salary; private Date startDate; private PhoneNumberVO[] phoneNumbers; // getter and setter methods not shown for brevity }
The PhoneNumberVO class referenced in EmployeeVO is defined as follows:
package samples.data; public class PhoneNumberVO { private String type; private String number; // getter and setter methods not shown for brevity }
When remotely invoking the addEmployee()
method in the client application, you need to make sure that the ActionScript object you pass as an argument is deserialized into an instance of the EmployeeVO class at the server side. Similarly, you need to make sure that the PhoneNumber objects you pass as part of the Employee object are deserialized into instances of the PhoneNumberVO class.
There are three basic rules to make sure an ActionScript object passed as a method parameter is deserialized into an instance of a specific Java class:
-
You need to declare the ActionScript/Java class mapping using the
Object.RegisterClass()
static function. For example:Object.registerClass("samples.data.EmployeeVO", samples.data.EmployeeVO);
The first argument is the fully qualified Java class name; the second argument is the ActionScript class.
- You have to provide the ActionScript class attributes with the exact same names (and same letter case) as the Java class attributes.
- You have to map the data types of the ActionScript class attributes to the data types of the Java class attributes. (Refer to the ActionScript/Java data type mapping table in the documentation for more details.)
In addition, even though it's not required, it's often considered a good practice to provide your ActionScript and Java classes with the same class and package names.
The same three rules also allow a Java object returned by remote method invocation to be deserialized into an object of a specific ActionScript class. (By default, an object returned from the server is deserialized into an instance of the ActionScript Object class.)
Here is the implementation of the Employee Directory using RemoteObject:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.macromedia.com/2003/mxml" backgroundColor="#FFFFFF" initialize="initApp()"> <mx:Script> import samples.data.*; var employee: EmployeeVO; function initApp() { employee=new EmployeeVO(); employee.phoneNumbers=new Array(); } function addEmployee() { employee.firstName=firstName.text; employee.lastName=lastName.text; employee.startDate=startDate.selectedDate; employee.salary=Number(salary.text); srv.addEmployee(employee); } </mx:Script> <mx:RemoteObject id="srv" source="samples.data.EmployeeService"> <mx:method name="addEmployee"/> </mx:RemoteObject> <mx:Form> <mx:FormItem label="First Name"> <mx:TextInput id="firstName"/> </mx:FormItem> <mx:FormItem label="Last Name"> <mx:TextInput id="lastName"/> </mx:FormItem> <mx:FormItem label="Salary"> <mx:TextInput id="salary"/> </mx:FormItem> <mx:FormItem label="Start Date"> <mx:DateField id="startDate"/> </mx:FormItem> <mx:FormItem label="Phone Numbers" verticalGap="1"> <mx:DataGrid id="dg" editable="true" dataProvider="{employee.phoneNumbers}" > <mx:columns> <mx:Array> <mx:DataGridColumn columnName="type" headerText="Type" cellRenderer="PhoneTypeRenderer"/> <mx:DataGridColumn columnName="number" headerText="Number"/> </mx:Array> </mx:columns> </mx:DataGrid> <mx:HBox horizontalGap="1" width="100%" horizontalAlign="right"> <mx:Button label="+" click="dg.addItem(new PhoneNumberVO('H'))" cornerRadius="0" borderThickness="0" width="20" height="20"/> <mx:Button label="-" click="dg.removeItemAt(dg.selectedIndex)" cornerRadius="0" borderThickness="0" width="20" height="20"/> </mx:HBox> </mx:FormItem> <mx:FormItem> <mx:Button label="Add Employee" click="addEmployee()"/> </mx:FormItem> </mx:Form> </mx:Application>
EmployeeVO.as is defined as follows:
class samples.data.EmployeeVO { public var firstName : String; public var lastName : String; public var phoneNumbers : Array; public var salary : Number; public var startDate : Date; static var registered= Object.registerClass("samples.data.EmployeeVO", samples.data.EmployeeVO); }
PhoneNumberVO.as is defined as follows:
class samples.data.PhoneNumberVO { public var number : String; public var type : String; static var registered= Object.registerClass("samples.data.PhoneNumberVO", samples.data.PhoneNumberVO); function PhoneNumberVO(type: String) { this.type=type; } }
Creating ActionScript classes based on existing Java classes can be a tedious and error-prone task. To facilitate my own development efforts, I created a simple utility that uses Java introspection to create automatically an ActionScript class based on a Java Value Object class. This utility, named java2as, is included in the supporting ZIP file in the Requirements section.
Using WebService
Just as with RemoteObject, when you invoke web services methods using the WebService component, you can pass objects back and forth (as the methods' input parameters and return value) between the client and the service.
To implement the Employee Directory application using the WebService approach, we will expose the EmployeeService class used in the RemoteObject example as a web service.
You can use your own web services engine to expose the EmployeeService class as a web service. If you use Axis, add the following service declaration to WEB-INF\server-config.wsdd:
<service name="EmployeeService" provider="java:RPC"> <parameter name="methodName" value="*"/> <parameter name="className" value="samples.data.EmployeeService"/> <beanMapping languageSpecificType="java:samples.data.EmployeeVO" qname="ns5:EmployeeVO" xmlns:ns5="http://www.macromedia.com/samples"/> <beanMapping languageSpecificType="java:samples.data.PhoneNumberVO" qname="ns6:PhoneNumberVO" xmlns:ns6="http://www.macromedia.com/samples"/> </service>
Note: Make sure that the ns:<number>
you use for each beanMapping
is unique within the WSDD file.
The web service's WSDL file describes the methods available as well as the complex data types used in the service. If you use Axis, you can access the WSDL file using the following URL:
http://localhost:8700/flex/services/EmployeeService?wsdl
Here are two interesting sections of the WSDL file for the EmployeeService web service:
The method declaration:
<wsdl:message name="addEmployeeRequest"> <wsdl:part name="employee" type="tns1:EmployeeVO" /> </wsdl:message>
The complex data types declaration:
<complexType name="PhoneNumberVO"> <sequence> <element name="number" nillable="true" type="xsd:string" /> <element name="type" nillable="true" type="xsd:string" /> </sequence> </complexType> <complexType name="EmployeeVO"> <sequence> <element name="firstName" nillable="true" type="xsd:string" /> <element name="lastName" nillable="true" type="xsd:string" /> <element name="phoneNumbers" nillable="true" type="impl:ArrayOf_tns1_PhoneNumberVO" /> <element name="salary" type="xsd:int" /> <element name="startDate" nillable="true" type="xsd:dateTime" /> </sequence> </complexType>
When you remotely invoke the addEmployee()
method on the web service, your only requirement is to pass an ActionScript object whose attributes' names and data types match the complexType definition in the WSDL file. (Refer to the ActionScript/Web Services data type mapping table in the documentation for more details.)
To implement the Employee Directory using the WebService approach, simply replace the RemoteObject declaration with a WebService declaration:
<mx:WebService id="srv" wsdl="@ContextRoot()/services/EmployeeService?wsdl"> <mx:operation name="addEmployee"/> </mx:WebService>
In this case, you don't have to define a class mapping using Object.registerClass()
. This means that you could also use the Employee and PhoneNumber class used in the HTTP example.
Summary
Flex provides three data services to accommodate the heterogeneous data access requirements of your application: HTTPService, RemoteObject, and WebService. Retrieving server data in a Flex application is generally easy. Sending complex data back to the server can be slightly more involved.
The HTTPServices allows you to send complex data to the server using XML over HTTP.
The RemoteObject service allows you to invoke methods remotely in Java objects deployed in your application server, and pass objects back and forth (as the methods' input parameters and return value) between the client and the server.
Similarly, the WebService component allows you to invoke web services methods and exchange complex data types between the client and the service.