R1-RPC/J User's Guide |
Version 1.2 July 2008 |
| Section | Sub-Sections |
|---|---|
| Introduction to R1-RPC/J |
The Issues and Needs of Flex-Java RPC The R1-RPC/J Solution The R1-RPC/J Toolkit and Development Process Test-Driven Development of Client-Server Applications What is in the R1-RPC/J Toolkit and How to Use It |
| Rules for Java Façade and VO Classes |
The Rules for Java Façade and VO Classes Server-Client Type Conversion and Mapping Accessing HTTP Request Objects in Façade Methods |
| RPC Programming with R1-RPC/J |
Server-Side Build and Deploy Creating the Client-Side Façade Counterpart Classes Creating and Using Façade Interfaces Client-Side Programming The Callback for RPC and Exception Handling |
| Using R1-RPC/J with Java Frameworks |
Using R1-RPC/J with the Spring Framework Using R1-RPC/J with Other Java Frameworks |
| Defining VO Classes |
Bindability of VOs and VO Properties Using VOs as a Part of the Client-Side Data Model VO Class Inheritance and Inner VO Classes |
| Understanding Login with R1-RPC/J |
The LoginHandler and User VO Authenticated Remote Methods The Client Login Programming |
| CHAP User Registration and Login |
The Challenge Handshake Authentication Protocol (CHAP) Using CHAP Login with R1-RPC/J |
| Aspect-Oriented Programming (AOP) for RPC | |
| Cooperative Polling and RPC-Piggyback Messaging Service | |
| Using R1-RPC/J for Flash RPC | |
| RPC Transport Considerations | |
| Automated RPC Unit Testing |
Test Case Specification Tags The Generated Test Suite Class Control the Order of Test Cases Using Login and Test Authenticated Methods Creating Password Digest for Unit Testing with CHAP Login Creating Custom Unit Test Cases Programmatically |
| Using R1-RPC/J Tools |
Using R1-RPC/J Command-Line Code Generation Tools Using R1-RPC/J Code Generation Ant Tasks |
Introduction to R1-RPC/J
The RiaOne RPC for Java, or R1-RPC/J, is an RPC bridge between Java™ server side and RIA client side, especially Flex™ and Flash™ (henceforward, Flex and Flash) programs. It is designed to be easy-to-use, robust, and versatile. As a key system componet in RIAs, R1-RPC/J is designed to elicit good system design, and foster good software engineering practices, such as test-oriented development, clear delineation of software responsibilities, and good maintainability. In addition to RPC, R1-RPC/J also supports user registration and authentication and handles session timeout. It completely frees application developers from such logistical system tasks, so that they can focus on developing business logic and killer user interfaces. For more general discussions, refer to related literature such as the R1-RPC/J White Paper and R1-RPC/J Frequently Asked Questions.
Before getting started with R1-RPC/J, let us first understand the problems it tries to address, and the ideas behind this solution.
The Issues and Needs of Flex-Java RPC
We know that Flex and Flash provide a number of remoting mechanisms. Some of them talk to Adobe's proprietary server technologies such as media streaming; most others are low-level network accessing including sockets, HTTP and SOAP web services. The data sent and received on both ends are typically of very primitive data types; as soon as big systems are getting developed with Flex, the need of remote data abstraction becomes obvious and imperative.
ActionScript 3 (AS3) is a strongly-typed object-oriented language. Its design and its implementation are undoubtedly influenced by Java. Programming to objects would be the most natural approach for Flex-Java applications. However, the reality is quite far from this seemingly innocent and reasonable wish, due to two major obstables — logitic and conceptual.
The first issue is the logitics of mapping data types and objects between
the two languages. For instance, Java has various generic
java.util. data structures; AS3 only has Object and
Array and a few collection classes. Also, Java has more primitive
data types than AS3.
The second issue is the confusion of object model serialization across different program spaces. This is a general problem for remoting mechanisms. Because objects inherantly contains and/or references other objects, any object in an object model is effectively a node in an object web. How to serialize a web of objects across different agents (client and server)? We must have a definitive answer to this before even designing an RPC solution.
The R1-RPC/J Solution
The R1-RPC/J solution is built upon an assumption that the server side exposes its services as methods in a simple Java façade class. The data that are transferred between the client and server are limited to simple data types, user-defined value objects (VOs), and container data structures such as Java arrays, maps and lists. All objects and containers must use containment of other objects, not object references.
This restrictions may seem limiting; in reality, nothing is really sacrificed. The reason is that, client and server programs are running in their own spaces; they both have very disparate object models for very different purposes. The client-side object model, for instance, is primarily to satisfy the needs of the UI and the remote service requests, where the server-side object model is for business logic. Passing object webs to the other end is neither necessary nor desired. What should be passed back and forth is the data necessary to establish or refresh the respective object models, and VOs are exactly appropriate for this purpose.
Server-side VO classes may seem like an overhead in the overall system; but chances are, many of these classes (or, more accurately, their generated client-side counterpart) can be used directly in the client program, so they effectively become part of the client object model. In other words, many of the VO classes are client-side model classes written on the server-side. For this reason, R1-RPC/J supports embedded AS3 code in javadoc comment within VO Java source code.
The R1-RPC/J Toolkit and Development Process
Figure 1 below shows the overall development landscape of a client-server application using R1-RPC/J as its RPC mechanism.
Figure 1. R1-RPC/J Application Architecture
The three green parts of the diagram are where you need to write code for. The client UI and server-side business logic are the rightful parts of the development effort. R1-RPC/J handles and hides the remote communication completely from the programmers with the only exception of the help that the developer provide Java façade and the related VO classes. The R1-RPC/J code generation tools generate AS3 classes that correspond to Java façade and VO classes. The generated classes are so close to their Java counterpart, you are practically calling Java from AS3 (plus a callback function required by AS3 for making asynchronous calls).
In addition to RPC, R1-RPC/J also supports user registration and login and user server session timeout control. You will need to provide a login handler for creating and manipulating user information, such as saving to and retrieving from a user database. R1-RPC/J sports a Challenge Handshake Authentication Protocol (CHAP), making the user authentication process secure enough for many medium-level secure web applications.
Another cool feature of R1-RPC/J is its AOP support for RPC. You can specify an AOP interceptor for a whole façade class or some of the methods; the interceptor will receive call-start, call-end, call-return and exception events. What to do with them is totally up to the developer.
Test-Driven Development of Client-Server Applications
R1-RPC/J can also generate unit test cases for each façade methods that have test case input and output present in their javadoc comments. Part of the Ant build script can automatically build a GUI tester for these test cases. This can be done any time a façade class is defined, even before the client program is started.
This automated RPC testing support is especially great for a better team development environment. At the earliest stage of development, the server team can provide a fake implementation and the client team can start developing UI right away through the interface. In the mid of development, RPC unit testing can quickly point out where really lies a bug. Throughout the life cycle of the project and deep into maintenance mode, they will safe guard any changes made to the server side. This is invaluable.
What is in the R1-RPC/J Toolkit and How to Use It
The R1-RPC/J software includes everything you need to develop with the R1-RPC/J solution. It includes the client-side and server-side libraries, code generation tools including Ant taskdef's, sample applications with Ant build scripts and the ready-to-run J2EE war files, and complete documentation.
The samples' build scripts are almost identical except for the Flash sample.
They are a good place to start; but you need to modify the entries in the
accompanying env.properties file to reflect your build
environment, such as SDK locations, application name, etc. The result of a
build is a J2EE war file, which includes the intended application and
an RPC unit testing application. The latter is valid only if some of the
façade methods have test case input and output specified; otherwise, it
would be useless because there would not contain any test cases.
The downloaded package is fully functional software. For production
deployment, you must purchase a valid license file, r1rpc.lic.
This file is to be put in the root of the Java CLASSPATH. For those samples,
simply keeping it with the build.xml file suffices. Without this
license file at runtime, your server is running in the evaluation
mode, which is perfectly good for developing purposes.
While Ant building is convenient, a manual development exercise is definitely helpful to understand the process better. The R1-RPC/J Tutorial provides a few step-by-step hands-on excercises to build and run some simple Flex- and Flash-Java applications. It is highly recommended that you do these first prior to reading on.
Rules for Java Façade and VO Classes
In R1-RPC/J, there are some simple rules governing the Java façade and VO classes. These rules help to clearly define the problem space, and constitute the foundation of this very solution.
The Rules for Java Façade and VO Classes
- The Java façade classes can not have overloaded methods.
Reason: because ActionScript 3 does not support class method overloading. - The Java façade and VO classes must implement signature interfaces
r1.rpc.RemoteFacadeandr1.rpc.RemoteVO, respectively.
Reason: for the code generation tool to pick them up and also documentation purposes. - Data structures over the wire can not have references, only containment.
Reason: because object referencing is a manifestation of object relationship, which is a part of an object/data model. Since server- and client-side object models are separate and not related, object referencing is meanless and hence prohibited to avoid confusion.
The Java façade methods' parameter and return types are constrained to these:
- Simple Java types, including all primitive types and their object wrappers,
java.lang.String,java.util.Date,java.util.Calendar,java.util.GregorianCalendar,java.sql.Date,java.sql.Time, andjava.sql.Timestamp. - Custom VO classes.
- The generic
java.lang.Objectclass; at runtime, the value must be of a type in this list. - Java arrays of any types in this list.
- Java container data structures in package
java.util, that contain elements of any types in this list. void(for method return type).
The Java VO classes have similar constraints:
- VO classes' properties can only have types in this list as well.
Server-Client Type Conversion and Mapping
Data types are mapped between Java and AS3 at code-generation time and runtime. The following table enumerates the Java types and their generated AS3 counterparts:
| Java Type | AS3 Type |
|---|---|
boolean, java.lang.Boolean |
Boolean |
byte, java.lang.Byte |
int |
float, java.lang.Float |
Number |
java.lang.String |
String |
java.util.Date |
Date |
*[] (except for char[] and Character[]) |
Array |
java.util.Map |
Object |
Custom VO Class |
Generated Custom VO Class |
Table 1. Server-Client Type Mapping
AS3 does not support set data structure, so it is recommended
not to use java.util.Set for façade method. In case
they do appear, they are serialized into Array on the client side.
Accessing HTTP Request Objects in Façade Methods
The façade class methods are regular Java methods. Usually the code
need to access the user object and possibly other HTTP request parameters. The
R1-RPC/J runtime engine stores many such objects in a thread-local storage. They
are accessible via the r1.rpc.WebTierUtils class, whose methods
are all static. The following is a list of its major methods:
Useful methods of r1.rpc.WebTierUtils:
void checkAuthentication();
HttpServlet getServlet();
ServletConfig getServletConfig();
ServletContext getServletContext();
HttpServletRequest getServletRequest();
HttpServletResponse getServletResponse();
HttpSession getSession();
Object getSessionAttribute(String key);
Object getUser();
Object getUser(HttpSession session);
boolean isLoggedIn();
void removeSessionAttribute(String key);
Object setSessionAttribute(String key, Object val);
void setUser(Object user);
RPC Programming with R1-RPC/J
Let us start with the simplest possible RPC use case. We like to provide a remote call in Java that says "Hello" to the incoming name. The following is the source code of the façade class:
package sample;
public class FriendlyFacade implements r1.rpc.RemoteFacade
{
/**
* @ASTestInput [ 'James' ]
* @ASTestResult x == 'Hello, James!'
*/
public String sayHi(String name) {
return "Hello, " + name + '!';
}
}
We notice that this is a plain Java class that implements the
r1.rpc.RemoteFacade interface, which is empty. The method for the
remote service, sayHi(), is just a regular method. The method's
special javadoc tags in comment are for unit testing, and will be discussed
later. Usually façade class methods are thin delegates to service
implementation code; in this simple example, we just implement it here.
Server-Side Build and Deploy
Compile this Java class, and put it into the appropriate J2EE web application deployment structure as follows:
{webapp_name}/WEB-INF/classes/sample/FriendlyFacade.class
{webapp_name}/WEB-INF/classes/r1rpc.lic
{webapp_name}/WEB-INF/lib/r1rpc-1.3.jar
{webapp_name}/WEB-INF/web.xml
The r1rpc.lic may be absent for development purposes. The
web.xml is like so:
<web-app ... >
<servlet>
<servlet-name>R1RPCServlet</servlet-name>
<servlet-class>r1.rpc.RPCServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>R1RPCServlet</servlet-name>
<url-pattern>/rpc</url-pattern>
</servlet-mapping>
</web-app>
This designates http://host:port/webapp/rpc
as the URL for RPCs from clients.
Creating the Client-Side Façade Counterpart Classes
As soon as a Java façade class is created or updated, you must run
the code generation tool to create the client-side counterpart. The tool is
a Java program, r1.tools.java2client.ASCodeGen; it requires
JDK_HOME/lib/tools.jar and the
Apache Velocity template engine.
Assuming r1tools.jar, JDK_HOME/lib/tools.jar,
and the Velocity template engine library jar files are in CLASSPATH, the tool's
command line is:
java r1.tools.java2client.ASCodeGen -src server_src -dest client_src sample
This will generate a sample/FriendlyFacade.as file in the
destination directory; its content is like this:
package sample
{
import r1.core.r1_internal;
import r1.rpc.RPC;
import r1.rpc.RPCFacade;
public class FriendlyFacade extends RPCFacade
{
/**
* @param rpc the RPC connection to use. If it is null,
* the default RPC connection singleton is used, which is
* initialized by a call to RPC.initSingleRPC().
* @param encoding the data encoding used in RPC calls for all the
* methods in this class. Default is 0 (RPC.RPC_DEFAULT).
*/
public function FriendlyFacade(rpc:RPC=null, encoding:int=0) {
super(rpc, encoding);
}
/////////////////////////////////////////////////////////////////////////////
//
// In the facade methods, the first parameter, receiver, can be one of
// the following:
//
// 1. A callback function with this signature:
//
// function callback(retVal:ReturnType=null, err:ErrorResult=null):void;
//
// 2. An array of [ host, field ], where host[field] is to receive the value.
// Failure will be handled by the system.
//
// 3. An array of [ host, field, failureCallback ], where host[field] is to
// receive the value, and in case of failure, the failureCallback will be
// invoked; this function has this signature:
//
// function failureCallback(err:ErrorResult):void;
//
// receiver can be null, meaning to use default error handling and
// ignore the status of the call or its return value, if any.
//
/////////////////////////////////////////////////////////////////////////////
/**
* If receiver is a callback, its signature is:
* function cb(returnValue:String, errMsg:String):void { }
*/
public function sayHi(receiver:*, name:String):void {
r1_internal::call("sayHi", [name], receiver);
}
} // end of class FriendlyFacade.
} // end of package.
The client-side method sayHi() is identical to the server-side
counterpart with the exception of the added first parameter,
receiver. This is required by the nature of AS3 remote calls, and
its use is well explained in the comment block.
The constructor is discussed below.
Creating and Using Façade Interfaces
The code generation tool, r1.tools.java2client.ASCodeGen, has
a -useFacadeInterface option to generate a façade
interface along with the class, and the class implments that interface.
This may be desired in certain system designs; one possibility is that the system uses a factory to provide different RPC mechanisms under different usage situations. Another possiblity is that the client team likes to or needs to start coding without a server, and they wish to fake the RPC on the client side. Using interface in this situation will completely shield client code from server-side dependancy.
Run the above command line with the added -useFacadeInterface
option, you will get these files generated:
package sample
{
public interface IFriendlyFacade
{
function sayHi(receiver:*, name:String):void;
}
}
package sample
{
import r1.core.r1_internal;
import r1.rpc.RPC;
import r1.rpc.RPCFacade;
public class FriendlyFacade extends RPCFacade implements IFriendlyFacade
{
public function FriendlyFacade(rpc:RPC=null, encoding:int=0) {
super(rpc, encoding);
}
public function sayHi(receiver:*, name:String):void {
r1_internal::call("sayHi", [name], receiver);
}
}
}
Client-Side Programming
From client, remote calls are sent to the server through an
r1.rpc.RPC object. This RPC object is initialized
with the remote call URL, and is serving as a channel; each façade is
associated with one RPC as specified in the constructor. A single
RPC can be used for any number of façades.
In most situations, a single channel is good enough for the whole
application. Class r1.rpc.RPC keeps a static defaultRPC
instance, which is initialized by the RPC.initSingleRPC() static
method. It takes the same parameters as RPC's constructor:
public function RPC(url:String, encoding:int=RPC_DEFAULT, sessionNeverTimeout:Boolean=true)
The first parameter, url, is the URL for RPC servlet on the
server-side; it can be relative. The encoding parameter specifies
the default transport encoding; it can be overridden by a façade. At
this writing, using RPC_DEFAULT is fine; more formats will be
developed in the near future. The last parameter, sessionNeverTimeout,
is boolean that when set to true, the server session never times out, as this
RPC will ping the server before it is idle for too long.
In the façade class's constructor, you can pass null
as its first parameter to indicate that this façade will use
RPC.defaultRPC for its remote calls; just make sure that
RPC.defaultRPC is initialized before any calls can be made.
Let us take a look at a real client program that uses our FriendlyFacade.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application layout="absolute"
xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*"
creationComplete="RPC.initSingleRPC('rpc')">
<mx:Script><![CDATA[
import r1.rpc.RPC;
import r1.rpc.RPCResult;
import sample.FriendlyFacade;
private var facade:FriendlyFacade = new FriendlyFacade();
private function sayHiCB(result:String, rpcResult:RPCResult):void {
if (!rpcResult.success) {
output.text = "Error: " + rpcResult.errorDetail.message
} else {
var txt:String = output.text;
if (txt == null || txt == '')
output.text = result
else
output.text = txt + '\n' + result
}
}
]]></mx:Script>
<mx:Form label="Direct Call" width="100%">
<mx:FormItem label="Name:" direction="horizontal">
<mx:TextInput id="myName"/>
<mx:Button label="Say Hi!"
click="facade.sayHi(sayHiCB, myName.text)"/>
</mx:FormItem>
<mx:FormItem label="Output:" width="100%">
<mx:TextArea id="output" width="100%" height="200"/>
</mx:FormItem>
</mx:Form>
</mx:Application>
This client program is a simple UI; it is deployed at the root of the J2EE
application, so the relative remote URL is "rpc". The rest of the
code is self-evident.
Figure 2. Simple RPC UI
The Callback for RPC and Exception Handling
The callback function for each remote call takes two parameters. The first
is the return value (null if it is a void function) if the call
returns successfully. The second parameter is an r1.rpc.RPCResult
object. If the call is successful, the same RPC.SUCCESS object is
returned; otherwise, an instance of r1.rpc.ErrorResult, which
extends RPCResult, is returned (and the return value parameter is
always null.) These two classes are essentially defined as:
public class RPCResult extends Event
{
public static const RPC_SUCCESS:String = "success";
public function RPCResult(type:String) { super(type); }
public function get success():Boolean { return this.type == RPC_SUCCESS }
public function get failed():Boolean { return !success }
public function get errorDetail():ErrorResult { return null }
}
public class ErrorResult extends RPCResult
{
private static const SYSERR_PREFIX:String = "SYSERR::";
public static const RPC_URL_NOT_SET :String = SYSERR_PREFIX + "RPC_URL_NOT_SET";
public static const RPC_IO_ERROR :String = SYSERR_PREFIX + "RPC_IO_ERROR";
public static const RPC_ERROR :String = SYSERR_PREFIX + "RPC_ERROR";
public static const SERVER_CONFIG_ERROR :String = "SERVER_CONFIG_ERROR";
public static const SERVER_AUTH_FAILURE :String = "SERVER_AUTH_FAILURE";
public static const SERVER_ERR_PARSE :String = "SERVER_ERR_PARSE";
public static const SERVER_ERR_NO_METHOD :String = "SERVER_ERR_NO_METHOD";
public static const SERVER_ERR_UNMARSHALL:String = "SERVER_ERR_UNMARSHALL";
public static const SERVER_ERR_MARSHALL :String = "SERVER_ERR_MARSHALL";
private var _message:String;
public function ErrorResult(type:String, msg:String) { super(type); _message = msg; }
public function systemError():Boolean { return type.indexOf(SYSERR_PREFIX) == 0 }
public function get message():String { return _message }
override public function get errorDetail():ErrorResult { return this }
}
Notice that r1.rpc.RPCResult is a
flash.events.Event. In fact, r1.rpc.RPC extends
flash.events.EventDispatcher; when a remote call fails, an event
is dispatched with the ErrorResult object. You can listen for and
handle specific error messages.
A remote call may fail for many reasons. The remote call may not go through
for whatever physical or network reasons; they are represented by
RPC_URL_NOT_SET, RPC_IO_ERROR, and
RPC_ERROR. The server may not function for a given request;
these are represented by the SERVER_xxxx names.
The SERVER_AUTH_FAILURE error is particularly interesting;
it happens when a user update fails or whenever a
javax.security.auth.login.LoginException occurs.
Beyond these system-level exceptions, the remote call itself may throw Java
exceptions. Server-side Java exceptions are returned on the client side as an
ErrorResult; the name is prefixed with "JAVA::". For instance,
if an exception of sample.MyException is thrown, the name of the
ErrorResult is "JAVA::sample.MyException".
Using R1-RPC/J with Java Frameworks
While the R1-RPC/J model of using façade pattern exerts little
disturbance to the server side programming, it can be more tightly integrated
into Java frameworks, so that the programming and configuration details are
more conforming to the overall server side coding standard. R1-RPC/J includes
built-in support for using the Spring
Framework, and includes the source code for both RPCServlet
and RPCSpringServlet as reference implementations should you need
to integrate with other frameworks.
Using R1-RPC/J with the Spring Framework
The façade pattern used exclusively by R1-RPC/J makes R1-RPC/J applications
perfect candidate for the Spring Frameworks. R1-RPC/J includes a
RPCSpringServlet, which extends
org.springframework.web.servlet.FrameworkServlet.
This Spring servlet class is contained in spring-webmvc.jar;
at the minimum, you will need that and spring-webmvc.jar deployed
in the webapp.
The key differences between Spring and non-Spring R1-RPC/J applications are:
- Configure the webapp to use
RPCSpringServlet, and - Must specify all façades, along with optional login and message handlers, as Spring beans. (This is hardly a requirement, because it is the very reason you want to use R1-RPC/J with the Spring framework in the first place.) The façade beans must use the class name, either fully qualified or the bare class name, for R1-RPC/J runtime engine to look up.
The following is a sample deployment of R1-RPC/J application with Spring.
{webapp_name}/WEB-INF/lib/r1rpc-1.3.jar
{webapp_name}/WEB-INF/lib/spring.jar
{webapp_name}/WEB-INF/lib/spring-webmvc.jar
{webapp_name}/WEB-INF/web.xml
{webapp_name}/WEB-INF/app-config.xml
The web.xml is:
<web-app ... >
<servlet>
<servlet-name>R1RPCSpringServlet</servlet-name>
<servlet-class>r1.rpc.RPCSpringServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>R1RPCSpringServlet</servlet-name>
<url-pattern>/rpc</url-pattern>
</servlet-mapping>
</web-app>
The app-config.xml is:
<beans>
<bean id="FriendlyService" class="sample.MyServices" />
<bean id="loginHandler" class="sample.MyLoginHandler" >
<property name="service" ref="FriendlyService" />
</bean>
<bean id="AuthenticatedFriendlyFacade"
class="sample.AuthenticatedFriendlyFacade">
<property name="service" ref="FriendlyService" />
</bean>
</beans>
There are setter and getter methods in both MyLoginHandler and
AuthenticatedFriendlyFacade classes for the service
property. Functionally, this example is identical to another example,
simple_login.
Using R1-RPC/J with Other Java Frameworks
In the R1-RPC/J distribution, under the resources/, you will find
some of the source code files. To create a custom servlet class to integrate
with a new Java framework of your choice, locate and refer to
RPCServlet.java and RPCSpringServlet.java. The R1-RPC/J
functionality is implemented in class RPCActions. Your servlet
class needs to do three things:
- It must implement the
RPCConfigurationinterface to provide methods to locateloginHandler,messageHandlerand façade objects at runtime. - Create an instance of
RPCActiosnat startup. - For HTTP requests, delegate service handling to the
RPCActions.service()method.
Defining VO Classes
VO classes must all implement the r1.rpc.RemoteVO interface,
and must comply with the rules.
VOs are meant to carry values, so typically they only have properties and
getter/setter methods; these are replicated on the client-side as class
properties, while regular methods are simply ignored. In case a property
also has a getter and/or setter defined, a single property is generated in
the client-side VO class. Note that for boolean getters, it must be defined
like any other getters; that is, methods such as isXXX() are
not considered getters, unlike the JavaBean convention.
The R1-RPC/J code generator supports a number of javadoc tags within Java
VO classes at both class and property levels. At class level, there are
@extends, @implements, @bindable, and
@postConstruction. At property level, there are
@bindable, and @private. In any javadoc comments,
AS3 code can be embedded in @as3 or @ActionScript
tags. The code will be copied in the generated AS3 class verbatim. Keep in
mind that javadoc comments must precede either a class, field, or method
declaration, not anywhere in the class body.
Bindability of VOs and VO Properties
The @bindable tag can precede either class or property and
getter/setter methods; it is converted to [Bindable] in the
generated code for the class or the property. The @private
tag signifies that the property would not be present in the client-side
class.
Using VOs as a Part of the Client-Side Data Model
The tags of @extends, @implements,
postConstruction, and @as3 are designed to produce
extra AS3 code in the client-side VO classes, in addition to the Java VO
properties. With these, the generated AS3 VO classes can be effectively as
UI-side object model classes.
The @extends and @implements tags allows the
generated AS3 class to extend and/or implement other regular AS3 classes and
interfaces; in the @as3 tags, you can write any extra code to
override or implement necessary methods.
The @postConstruction tag takes AS3 code; this code will be
copied to the end of the constructor in the generated AS3 class. Since VOs
cannot have object references during serialization, this post construction
code can be used to invoke client-side functions to update the intended part
of object model.
Let us see an example. The following shows a regular AS3 class and an
interface in package test.exotic:
public class MyBase
{
public function getName():String { return null }
}
public interface MyItf
{
function get description():String;
}
The following is a server-side VO class:
package test;
/**
* @extends test.exotic.MyBase
* @implements test.exotic.MyItf
* @bindable
* @postConstruction
* trace("MyChildExoticVO: name=" + this.name)
*
* @ActionScript
* public function get description():String { return "MyChildExoticVO" }
* public override function getName():String { return name }
*/
public class MyChildExoticVO implements r1.rpc.RemoteVO
{
public String name;
}
The generated client-side VO class is:
package test
{
import r1.rpc.RPCUtils;
import test.exotic.MyItf;
import test.exotic.MyBase;
[Bindable]
public class MyChildExoticVO extends test.exotic.MyBase
implements test.exotic.MyItf
{
public var name:String;
public function MyChildExoticVO(init:Object=null) {
super();
if (init != null)
RPCUtils.populate(this, init);
trace("MyChildExoticVO: name=" + this.name)
}
public function get description():String { return "MyChildExoticVO" }
public override function getName():String { return name }
}
}
VO Class Inheritance and Inner VO Classes
R1-RPC/J supports VO class inheritance, including extending abstract VO classes. Abstract VO classes are also generated on the client-side for derived VO classes. At runtime, abstract VO classes should not be instantiated and never be passed in remote calls to the server.
Static inner VO classes inside a VO class are supported. See the example below.
package test.livingThing;
public abstract class Animal implements r1.rpc.RemoteVO
{
private String _name;
public String getName() { return _name; }
public void setName(String n) { _name = n; }
public abstract String getSkinType();
public boolean getHasFur() { return _skinIs("fur"); }
public boolean getHasFeather() { return _skinIs("feather"); }
private boolean _skinIs(String s) { return s.equals(getSkinType()); }
// Inner class
public static class Panda extends Animal
{
public String getSkinType() { return "fur"; }
// Inner class
public static class RentedPanda extends Panda
{
public boolean getIsRented() { return true; }
}
}
// Inner class
public static class Parrot extends Animal
{
public String getSkinType() { return "feather"; }
}
} // end of class Animal.
}
The inner classes will be generated on the client side with a dollar
sign ($) between the names of the inner and outer class. Otherwise, they
are no difference then other AS3 classes. For instance, the generated
Animal$Panda$RentedPanda looks like this:
package test.livingThing
{
import r1.rpc.RPCUtils;
public class Animal$Panda$RentedPanda extends Animal$Panda
{
public var isRented:Boolean;
public function Animal$Panda$RentedPanda(init:Object=null) {
super();
if (init != null)
RPCUtils.populate(this, init);
}
}
}
Understanding Login with R1-RPC/J
User authentication is virtually required in every enterprise application. R1-RPC/J runtime engine includes a complete user authentication system with Challenge Handshake Authentication Protocol (CHAP). With CHAP, a dynamic digest of the password texts are passed across the wire, never the password itself. This should suffice for many applications, especially when coupled with secure connection schems such as encoded transport.
For the convenience of development, a plain-text password simple login system is also supported. Its programming is similar to that of CHAP, but is easier to understand. This is where we start.
The LoginHandler and User VO
To enable user authentication in an R1-RPC/J-based system, first
you need to provide a LoginHandler and specify it in
the web.xml like so:
<web-app ... >
<servlet>
<servlet-name>R1RPCServlet</servlet-name>
<servlet-class>r1.rpc.RPCServlet</servlet-class>
<init-param>
<param-name>loginHandlerClass</param-name>
<param-value>sample.MyLoginHandler</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>R1RPCServlet</servlet-name>
<url-pattern>/rpc</url-pattern>
</servlet-mapping>
</web-app>
The login handler class must extend r1.rpc.LoginHandler,
which has these methods for you to override and implement:
public String getPasswordDigest(String id) throws PasswordNotRetrievableException
{ throw PasswordNotRetrievableException.THE_ONE; }
public abstract void savePasswordDigest(String id, String digest);
public abstract String createUser(String id, String digest, Object data);
public abstract Object loadUser(String id, String password_digest);
protected boolean doEncrypt() { return true; }
The underlying login implementation handles two scenarios: (a) user password
can be retrieved and compared, and (b) user password can not be retrieved but
is used to load the user object. Either way, at the end of a successful login,
a user object is obtained and stored in the session. In the first scenario,
where the user password (or digest) is stored in a database, both
getPasswordDigest(id) and loadUser(id, password) are
used, but the password parameter in the latter is typically ignored.
The second scenario is that password is not stored in a database; for instance,
the user is authenticated using the operating system or network domain account.
In this case, getPasswordDigest(id) is left untouched (which
throws the PasswordNotRetrievableException), and
loadUser(id, password) is solely responsible for obtaining the
user object using both id and password values.
Methods createUser(), savePasswordDigest() and
getPasswordDigest() are for user registration and login using
CHAP. This is typically necessary because with CHAP, only password digests are
transmitted across the wire but never the passwords themselves. It is possible
(and often necessary) to generate user password digests off-line, but on-line
user registration is probably desired in web applications. More on CHAP later.
The return value of doEncrypt() indicates whether CHAP login
is used or not. It defaults to use CHAP.
In this section, we do not use CHAP and simply hard-code the user information in memory. The login handler is very simple:
package sample;
public class MyLoginHandler extends r1.rpc.LoginHandler
{
protected boolean doEncrypt() {
return false;
}
public String getPasswordDigest(String id) {
return MyServices.getPasswordDigest(id);
}
public void savePasswordDigest(String id, String digest) {
MyServices.savePasswordDigest(id, digest);
}
public String createUser(String id, String digest, Object data) {
return null; // not used.
}
public Object loadUser(String id, String digest) {
return MyServices.getUser(id);
}
}
The logic of user authentication is delegated to a service class,
MyServices. It returns a user object of class UserVO,
which is a VO class itself:
package sample;
public class UserVO implements r1.rpc.RemoteVO
{
public String id;
/**
* @bindable
*/
public String name;
public String desc;
/**
* @private
*/
public String password;
public UserVO(String id, String password, String name, String desc) {
this.id = id;
this.password = password;
this.name = name;
this.desc = desc;
}
}
Authenticated Remote Methods
R1-RPC/J runtime engine uses annotation to enforce remote methods authenticated:
package sample;
import r1.rpc.LoginHandler;
import r1.rpc.RemoteFacade;
import r1.rpc.aop.RPCInterceptor;
@RPCInterceptor(checkAuthentication="yes")
public class AuthenticatedFriendlyFacade implements RemoteFacade
{
public String sayHi() {
return "Hello, " + ((UserVO)LoginHandler.getUser()).name + '!';
}
}
In this example, all methods in façade
AuthenticatedFriendlyFacade are authenticated. What happens on
the server is that, after the user has logged in, the user object is stored
in the session; this user object can be retrieved via the static
LoginHandler.getUser() method. If a remote method needs be
authenticated, R1-RPC/J runtime checks the availability of the user object
for the session; if the user object is not found, hence the user is not
logged in, a javax.security.auth.login.LoginException is thrown.
The Client Login Programming
On the client side, the r1.rpc.RPC class has a
login() and logout(). The following code is
for a simply UI with two screens:
<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://www.adobe.com/2006/mxml"
creationComplete="init()" layout="absolute">
<Script><![CDATA[
import mx.controls.Alert;
import r1.rpc.RPC;
import r1.rpc.RPCResult;
import r1.rpc.ErrorResult;
import sample.UserVO;
import sample.AuthenticatedFriendlyFacade;
private var facade:AuthenticatedFriendlyFacade = new AuthenticatedFriendlyFacade;
[Bindable] public var user:UserVO;
private function init():void {
RPC.initSingleRPC('rpc');
RPC.defaultRPC.passwordEncrypted = false; // default is true.
}
private function doSignIn():void {
RPC.defaultRPC.login([this, 'user', errorHandler], username.text, password.text);
password.text = '';
}
private function doSignOut():void {
user = null;
RPC.defaultRPC.logout();
}
private function cb(result:String, rpcResult:RPCResult):void {
if (!rpcResult.success) {
errorHandler(rpcResult.errorDetail);
} else {
Alert.show('Returned: ' + result);
}
}
private function errorHandler(err:ErrorResult):void {
Alert.show('[' + err.type + '] ' + err.message);
}
]]></Script>
<ViewStack width="100%" height="100%" selectedIndex="{user==null ? 0 : 1}">
<Form label="Login" width="100%">
<FormItem label="Name:">
<TextInput id="username" text="test"/>
</FormItem>
<FormItem label="Password:">
<TextInput id="password" text="test" displayAsPassword="true"/>
</FormItem>
<FormItem>
<Button label="Sign In" click="doSignIn()"/>
</FormItem>
<FormItem>
<Button label="Try to Say Hello" click="facade.sayHi(cb)"/>
</FormItem>
</Form>
<VBox width="100%" height="100%">
<Label id="caption" text="Welcome, {user.name}!" fontSize="20"/>
<Spacer height="10"/>
<Button label="Say Hello to Myself" click="facade.sayHi(cb)"/>
<Button label="Sign Out" click="doSignOut()"/>
</VBox>
</ViewStack>
</Application>
The first screen is for login:
Figure 3. The Simple Login Sample Screen
If you click on the "Try to Say Hello" button, an exception will be received because the user has not logged in yet. Clicking on the "Login" button, it will go to the second screen; then, the "Say Hello to Myself" button will return the expected message:
Figure 4. The Second Screen of Simple Login
CHAP User Registration and Login
The Challenge Handshake Authentication Protocol (CHAP)
Having passwords transmitted as plain-text is not a comfortable thought. CHAP is a scheme that makes login process significantly safer because: a) Password never travels across the wire, and b) Clear password text is not stored on the server. It works like this.
- When the users register and create a new account, they type in a password. The client side sends the digest of the password, along with other information, to the server, and the server stores this password digest.
- Before the user starts to login, the server first sends a challenge, a random seed, to the client.
- The user types in the user ID and password and tries to login. The client side first gets the digest of the user password, and combines this hashed value with the challenge, and calculates a new digest, which is sent to the server.
- The server side uses the user ID to retrieve the stored user's password digest, then use the same challenge value to do another hashing just like the client side did, and compare the values. If they match, the user is successfully authenticated.
In short, CHAP only stores the digest of user passwords; during the login process, CHAP uses a dynamic challenge to further hash the password digest. It would take a lot of monitoring and computation to crack this scheme. CHAP login is quite secure for most applications.
Using CHAP Login with R1-RPC/J
R1-RPC/J runtime engines intrinsically handle CHAP login. All you have to do is,
like in simple login situation, provide your own LoginHandler;
most likely, you need to implement the createUser() method.
Because R1-RPC/J is defaulted to use CHAP, you actually have to do less
initialization as compared to the simple login usage.
package sample;
public class MyLoginHandler extends r1.rpc.LoginHandler
{
public String getPasswordDigest(String id) {
return MyServices.getPasswordDigest(id);
}
public String createUser(String id, String digest, Object data) throws Exception {
return MyServices.createUser(id, digest, (UserVO)data);
}
public void savePasswordDigest(String id, String digest) {
MyServices.savePasswordDigest(id, digest);
}
public Object loadUser(String id, String digest) {
return MyServices.getUser(id);
}
}
The createUser() method returns the ID for the new user. The
method takes an id as parameter; if it is not null,
the login handler should check for its availability before creating a user for
the ID. Otherwise, the login handler should create a new ID. The following
sample UI registers a new user, then shows the login page with the new ID for
user to login.
Figure 5. The CHAP Login Sample Screens
The source code is listed below:
<?xml version="1.0" encoding="utf-8"?>
<Application layout="absolute"
xmlns="http://www.adobe.com/2006/mxml"
creationComplete="RPC.initSingleRPC('rpc')">
<Script><![CDATA[
import mx.controls.Alert;
import r1.rpc.RPC;
import r1.rpc.RPCResult;
import r1.rpc.ErrorResult;
import sample.UserVO;
[Bindable] public var user:UserVO;
[Bindable] public var initUsername:String;
private function doRegister():void {
var regcb:Function = function(id:String, rpcResult:RPCResult):void {
if (rpcResult is ErrorResult) {
errorHandler(rpcResult as ErrorResult);
return;
}
Alert.show("User registered successfully.");
viewStack.enabled = true;
initUsername = id;
regUsername.text = regPassword.text = null;
tabNav.selectedIndex = 1;
}
var u:UserVO = new UserVO({ name:regUsername.text, desc:regDesc.text });
RPC.defaultRPC.registerUser(regcb, null, regPassword.text, u);
}
private function doSignIn():void {
RPC.defaultRPC.login([this, 'user', errorHandler], username.text, password.text);
password.text = '';
}
private function doSignOut():void {
user = null;
RPC.defaultRPC.logout();
}
private function errorHandler(err:ErrorResult):void {
Alert.show('[' + err.type + '] ' + err.message);
}
]]></Script>
<TabNavigator id="tabNav" width="100%" height="100%">
<Form label="Register New User" width="100%">
<FormItem label="New User Name:">
<TextInput id="regUsername"/>
</FormItem>
<FormItem label="New User Password:">
<TextInput id="regPassword"/>
</FormItem>
<FormItem label="More Information:">
<TextInput id="regDesc"/>
</FormItem>
<FormItem>
<Button label="Register" click="doRegister()"/>
</FormItem>
</Form>
<ViewStack id="viewStack" label="Login" width="100%" height="100%"
selectedIndex="{user==null ? 0 : 1}" enabled="false">
<Form label="Login" width="100%">
<FormItem label="Name:">
<TextInput id="username" text="{initUsername}" />
</FormItem>
<FormItem label="Password:">
<TextInput id="password" displayAsPassword="true"/>
</FormItem>
<FormItem>
<Button label="Sign In" click="doSignIn()"/>
</FormItem>
</Form>
<VBox width="100%" height="100%">
<Label id="caption" text="Welcome, {user.name}!" fontSize="20"/>
<Spacer height="10"/>
<Button label="Sign Out" click="doSignOut()"/>
</VBox>
</ViewStack>
</TabNavigator>
</Application>
Aspect-Oriented Programming (AOP) for RPC
Remote calls can be intercepted and processed by the R1-RPC/J runtime
engine, when the RPCInterceptor has its handler
value set to a interceptor class name. An interceptor class must implement
r1.rpc.aop.RPCCallInterceptor, which has these methods:
package r1.rpc.aop;
import java.lang.reflect.Method;
public interface RPCCallInterceptor
{
void preCallHandler(Method method, Object[] params);
void postCallHandler(Method method, Object returnValue);
void exceptionHandler(Method method, Throwable t);
}
The @RPCInterceptor annotation applies to both façade
class and methods. The following sample façade class have all
methods intercepted except for method sub(), which has
annotation @RPCInterceptor(handler="none").
package sample;
import r1.rpc.LoginHandler;
import r1.rpc.aop.RPCInterceptor;
@RPCInterceptor(checkAuthentication="yes", handler="sample.MyCallInterceptor")
public class AuthenticatedFriendlyFacade implements r1.rpc.RemoteFacade
{
public String sayHi() {
return "Hello, " + ((UserVO)LoginHandler.getUser()).name + '!';
}
public int add(int a, int b) {
return a + b;
}
@RPCInterceptor(handler="none")
public int sub(int a, int b) {
return a - b;
}
public void forceMyException() throws MyException {
throw new MyException("A custom exception.");
}
}
The interceptor class sample.MyCallInterceptor simply prints
out some verbiage to System.out.
This AOP feature can be useful for special needs, such as profiling, logging, etc.
Cooperative Polling and RPC-Piggyback Messaging Service
R1-RPC/J conveniently supports a client-polling messaging service. Once a
messaing callback is registered to an RPC object is established
on the client side, any request from the client to the server will bring back
any waiting messages for the caller. The way to register a messager listener
is like this:
function listener(result:RPCResult):void {
for each(var msg:Object in result.serverMessages) {
// process msg ...
}
}
rpc.addEventListener(RPCResult.RPC_MESSAGE, listener);
On the server side, much like the LoginHandler, you implement
the interface for MessageHandler and specify it with the
RPCServlet in web.xml; that message handler class
can connect with any messaging mechanisms on the server side. The
MessageHandler has only one method, getMessages(),
that returns an array of valid RPC values such as primitives or VO classes.
The RPC, which is the underlying transport support for any
façade classes, has two features designed to facilicate messaging. The
first is the ping(). It simply sends an empty request to the
server; when there are messages waiting, they will be passed back. The program
can call this method to explicitly do a refresh, which can be triggered by a
user action such as Button or LinkButton. The second feature is the
pollingMaxDelaySeconds property; when set, R1-RPC/J will call
ping() if remote accessing has been idle for too long. Note that
this is the maximum delay; if some remote method call has happened, the timer
is reset because all messages are already piggybacked in the call return.
The messaging support is best demonstrated with a chat application. Please refer to the Chat sample for detail. Because of its nature, this sample is more involved with UI and flow, but the messaging support itself is quite straightforward.
Using R1-RPC/J for Flash RPC
R1-RPC/J client library is pure ActionScript 3, and is tested with Flash CS3. The following is a simple Flash test program that functionally does the same as Simple RPC example mentioned above.
Figure 6. Screenshot of the Flash RPC Sample
To create the Flash program using R1-RPC/J, first make R1-RPC/J's classes avaiable to
Flash CS3 by copying the r1rpc-?.?.swc library to your Flash CS3
components directory. On Windows XP, it is
boot drive\Documents and Settings\username\Local Settings\Application Data\Adobe\Flash CS3\language\Configuration\Components;
on Mac OS X, it is
Macintosh HD/Users/username/Library/Application Support/Adobe/Flash CS3/language/Configuration/Components.
In Flash CS3, open Window|Components and double-click (on Windows) on all
R1-RPC/J classes to import them into the library. Drag the used classes (called
components in Flash CS3 parlance) onto the stage. Copy the generated
façade and VO classes into Flash's class path, and use the following
code in the key frame:
import r1.rpc.RPC;
import r1.rpc.RPCResult;
import sample.FriendlyFacade;
RPC.initSingleRPC('rpc');
var facade:FriendlyFacade = new FriendlyFacade();
function sayHiCB(result:String, rpcResult:RPCResult):void {
if (!rpcResult.success)
output.text = 'Error: ' + rpcResult.errorDetail.message
else
output.text = result
}
input.addEventListener(KeyboardEvent.KEY_DOWN,
function(e:KeyboardEvent):void {
if (e.keyCode = Keyboard.ENTER)
facade.sayHi(sayHiCB, input.text)
});
stop();
RPC Transport Considerations
As shown in figure 1, the RPC data that goes across
the wire is handled by the R1-RPC/J engines on both ends; application
programmers do not need to handle nor care about the data format. This format
can be controlled by the encoding parameter in the constructor
of r1.rpc.RPC or the façade classes. Currently, the data
format for transport is defaulted to JSON format; in the future, more
efficient binary formats will be implemented when legal concerns and other
hurdles are cleared. Atop binary, the data transport can even be encrypted,
so that you can effectly use HTTP for secure application transactions.
It would be good if you leave the RPC and façade
constructors juse use RPC.RPC_DEFAULT; in the next upgrade of
the R1-RPC/J library, the transport may be defaulted to a more efficient
format, and you do not need to change a single character of the source code
to take advantage of that.
Automated RPC Unit Testing
One of the great advantages of R1-RPC/J is its automated RPC unit testing. An RPC bridge is not only a system component; in a collaborate development environment, it also bridges the client UI team and the server team. The ease of RPC unit testing is invaluable at various phases of team development. Before we discuss the details of how to specify test cases in the façade class, let us first see the automatically generated test application UI.
Figure 7. Screenshot of the RPC Unit Test Application UI
Test Case Specification Tags
In a façade class, unit test case for each method is specified via special javadoc tags. There are tags at method- and class-level. These tags are case-insensitive. The following table lists method-level tags.
| Test Case Tag | Enclosed Tag | Description |
|---|---|---|
@ASTest |
Used with the @ASTestEnd tag to specify multiple test case attributes using the abbreviated enclosed tags. | |
@ASTestEnd |
The end tag for @ASTest. | |
@ASTestName |
@name | The optional test case name. Default is the method name. |
@ASTestInput |
@input | The input to the AS3 method. Is an array of parameters. |
@ASTestResult |
@result | The result checking logic. The variable 'x' holds the return value. |
@ASTestNegative |
@negative | A boolean value, where true indicates this is a negative test. |
@ASTestComment |
@comment | The optional comment. |
@ASTestFirst |
@first | When specified with a positive number, this test case will be run prior to other test cases, in the order specified. |
@ASTestLast |
@last | When specified with a positive number, this test case will be run after all other test cases, in the order specified. |
@ASTestIgnore |
@ignore | When specified, this method will not have test case generated. |
Table 2. Unit Testing Method-Level Tags
The most used tags are @ASTestInput and @ASTestResult.
If these two are not specified, the test case will not be generated for the
method. Another useful tag is @ASTestNegative, which signifies
that this is a negative test case that should raise an exception.
Sometimes you want to ignore a test case but like to keep the test case
attributes. You can explicitly add a @ASTestIgnore to achieve
that.
If many of the test case attributes are specified, you can group them
within a pair of @ASTest and @ASTestEnd tags;
inside, use the abbreviated forms of the tags (those listed in the
Enclosed Tag column in the table above.)
The Generated Test Suite Class
A test suite class is generated for each façade class.
The test suite class normally extends r1.rpc.testing.RPCTestSuite;
this can be overridden by the façade class-level tag,
@ASTestParentClass, in which case the parent class must extend
r1.rpc.testing.RPCTestSuite. In any case, you can specify extra
code for the test suite class for any test cases to use in the tag
@ASTestGlobal. The following table lists all class-level tags:
| Test Case Tag | Description |
|---|---|
@ASTestGlobal |
The code to be copied verbatim into the test suite class. |
@ASTestParentClass |
The parent class for the test suite class. It must extend r1.rpc.testing.RPCTestSuite. |
@ASTestUsername |
If specified, this is the user name (actually, user ID) for login. |
@ASTestPassword |
The password for login. |
@ASTestPasswordEncrypted |
A boolean value indicating whether RPC should use CHAP or plain-text password for login. Default is true. |
Table 3. Unit Testing Class-Level Tags
The following is an exerpt of a Java façade class:
package test;
/**
*
* @ASTestGlobal
* public static function createVO():MyTestVO {
* return new MyTestVO({
* valboolean: true,
* valdouble: 8.08,
* valfloat: 9.09,
* valint: 10,
* vallong: 11,
* valshort: 12,
* valBoolean: false,
* varDouble: 1,
* valFloat: 2,
* valInteger: 3,
* valLong: 4,
* valShort: 5,
* valString: "xyz",
* varVector: [ "a" ],
* varDate: new Date(),
* valVO: createVO1()
* });
* }
*/
public class BasicTestFacade implements r1.rpc.RemoteFacade
{
/**
* @ASTestResult x.valint == 3
*/
public MyTestVO test_VO_out() { return new MyTestVO(); }
/**
* @ASTestInput [ createVO() ]
* @ASTestResult x.valint == 10
*/
public MyTestVO test_VO_in(MyTestVO x) { return x; }
}
By running the following command line tool, a corresponding test suite class is generated (list below):
java r1.tools.java2client.ASTestGen -src server_src -dest client_src sample
package test
{
import r1.rpc.testing.RPCTestSuite;
import r1.rpc.testing.RPCTestCase;
public class BasicTestFacadeTestSuite extends RPCTestSuite
{
//---------- BEGIN - Custom Global Code ----------
public static function createVO():MyTestVO {
return new MyTestVO({
valboolean: true,
valdouble: 8.08,
valfloat: 9.09,
valint: 10,
vallong: 11,
valshort: 12,
valBoolean: false,
varDouble: 1,
valFloat: 2,
valInteger: 3,
valLong: 4,
valShort: 5,
valString: "xyz",
varVector: [ "a" ],
varDate: new Date(),
valVO: createVO1()
});
}
public static function createVO1():MyTest1VO {
return new MyTest1VO();
}
//---------- END - Custom Global Code ----------
public function BasicTestFacadeTestSuite() {
super(new BasicTestFacade);
var tc:RPCTestCase;
//--- Test Case: test_VO_out ---
this.allTestCases.push(tc = new RPCTestCase(this, 'test_VO_out', null, false));
tc.callFacade = function(facade:BasicTestFacade, cb:Function):void {
facade.test_VO_out(cb);
}
tc.checkResult = function(x:datatype_test.MyTestVO, testCase:RPCTestCase):Boolean {
return x.valint == 3;
}
//--- Test Case: test_VO_in ---
this.allTestCases.push(tc = new RPCTestCase(this, 'test_VO_in', null, false));
tc.paramsGenerator=function(testCase:RPCTestCase):Array {
return [ createVO() ]
}
tc.callFacade = function(facade:BasicTestFacade, cb:Function):void {
var params:Array=this.paramsGenerator(this);
facade.test_VO_in(cb, params[0]);
}
tc.checkResult = function(x:datatype_test.MyTestVO, testCase:RPCTestCase):Boolean {
return x.valint == 10;
}
}
}
}
The content of the @ASTestInput and @ASTestResult
tags are copied into two generated functions,
paramsGenerate(testCase:RPCTestCase):Array and
checkResult(x:ReturnType, testCase:RPCTestCase):Boolean,
respectively. You can write any code for these two tags, as long as at the
end they return the appropriate value; in the case of simple expressions, the
R1-RPC/J test generation tool adds the return keyword. The
parameter testCase can be used in the code. Test suite's data
members, such as userObject, are accesible, too.
Control the Order of Test Cases
The @ASTestFirst and @ASTestLast tags allows you
to control the order of test case order in the test suite. The test suite
executes test cases in the order they are created during batch runs. Both tags
take a number; test cases with @ASTestFirst are run before those
without this tag, and those with @ASTestLast run after others. If
these tags may take a same sequence number for multiple test cases (methods).
This can be useful when some test cases depends on the results of other test
cases.
Using Login and Test Authenticated Methods
For authenticated façade classes and methods, the test UI will show
a Login button instead of the Run All button, unless valid
test user name and password are already specified as
@ASTestUsername and ASTestPassword in the
fa&ccedi;ade class. A successful login operation will keep the returned user
object in the property, userObject, which is accessible to all
test case methods such as code in @ASTestInput and
@ASTestResult. The following is an example.
package sample;
import r1.rpc.LoginHandler;
import r1.rpc.aop.RPCInterceptor;
/**
* @ASTestUsername test
* @ASTestPassword test
* @ASTestPasswordEncrypted false
*
* @ASTestGlobal
* public function get userVO():UserVO { return userObject as UserVO }
*/
@RPCInterceptor(checkAuthentication="yes")
public class AuthenticatedFriendlyFacade implements r1.rpc.RemoteFacade
{
/**
* @ASTestInput
* testCase.message = "User logged on with ID: test.";
* return [];
* @ASTestResult
* testCase.message = "User name: " + userVO.name;
* return x == 'Hello, ' + userVO.name + '!';
*/
public String sayHi() {
return "Hello, " + ((UserVO)LoginHandler.getUser()).name + '!';
}
}
The getter method for userVO also serves the purpose of making
sure that class sample.UserVO is linked in; if you just use the
generic userObject, a runtime exception may be raised for the
missing class of sample.UserVO.
Test User Registration for CHAP Login
When test user name and password are not specified, or they are not valid, the test user need to login. In applications with CHAP login, you may need to register a test user first. Both cases are implemented by the Login and Registration screen below.
Figure 8. The Test User Login/Registration Screen
When the user types in the name and password and click on the "Register and
Login" button, the following function is called in Login.mxml.
private function doRegisterAndLogin():void {
var userObj:Object = null;
// If you have more user information for registration,
// add them here and/or extend the UI for more fields.
testSuite.registerUserAndLogin(testerUI.postLogin,
username.text, password.text, userObj);
doCancel();
}
Note that the r1.rpc.RPC supports registering a user with more
information than name and password. If this is the case with your application,
you may have to modify Login.mxml to incorporate extra fields for
user registration.
Creating Password Digest for Unit Testing with CHAP Login
In the case of CHAP login, only the password digest is stored on the
server side. For any given password used in the test suite, a digest must be
generated in the user database somehow. The Java program
r1.util.CreateDigest does just that. For instance, the password
"test" has a digest of "qUqP5cyxm6YcTAhz05Hph5gvu9M=". Stick this text in the
user database for user ID of "test", and the above façade tests will
work fine, except that @ASTestPasswordEncrypted tag needs be
removed.
Creating Custom Unit Test Cases Programmatically
You may want to take advantage of the unit test framework and create more
complex unit test cases programmatically; such test cases may not even be
façad related. Use classes RPCTestCase and
RPCTestSuite in package r1.rpc.testing for this
purpose. The best way to start is probably refer to the generated test suite
classes. The API documentation is another important source.
The source code of the R1-RPC/J automated test framework UI is available in the package. There are these files:
testsuites.inc Main.mxml r1/rpc/testing/ui/TesterUI.mxml r1/rpc/testing/ui/Login.mxml r1/rpc/testing/ui/AboutBox.mxml
The testsuites.inc file is generated when all façade
classes are processed. An example is as follows:
import test.BasicTestFacadeTestSuite;
import test.DeepTestFacadeTestSuite;
import test.ExoticClassesFacadeTestSuite;
import test.GenTestFacadeTestSuite;
[Bindable] public var testSuites:Array = [
new BasicTestFacadeTestSuite,
new DeepTestFacadeTestSuite,
new ExoticClassesFacadeTestSuite,
new GenTestFacadeTestSuite
];
[Bindable] public var appTitle:String = 'R1RPC Datatype Testing - RPC Unit Testing';
public var rpcUri:String = '../rpc';
Just add your test suite classes to the testSuites variable,
and build the UI. It is that easy.
In the AboutBox.mxml, you can put in informative notes that
is useful for the test runners.
Using R1-RPC/J Tools
Using R1-RPC/J Command-Line Code Generation Tools
We have seen the usages of code and test case generation above. These two
tools, ASCodeGen and ASTestGen, are an integral part of the
R1-RPC/J Toolkit. They are implemented as java doclets for retrieving javadoc
tags in the Java source files; they use the Apache Velocity template engine
to spit out AS3 source files; therefore, at the time of running the tools,
you must have JDK's tools.jar, the Velocity library jar files,
and the R1-Tools library file, r1tools.jar, in the classpath.
Both tools share a same set of command-line options with the only exception
of -useFacadeInterface for the ASCodeGen tool. If you
run either of the tools without parameters, help information is printed:
C:\>java r1.tools.java2client.ASCodeGen
Running ASCodeGen doclet to generate remote facade and VO classes.
usage: java r1.tools.java2client.ASCodeGen
<Java_Packa>|<Java_Source_Files>
-subpackages Optional. When specified, takes package names
separated by colon; all sub-packages are included also.
An alternative to specifying packages or source file names.
-exclude Optional. When specified in the same format as
-subpackages, those package are excluded.
-dest base directory for generated test client soure files
-exceptionOnError when specified, throws an exception on error (used
by the ant task).
-src base directory for Java source files
-useFacadeInterface when specified, the generated facade class will
implment a generate interface.
-verbose when specified, display more information in the process.
The way to specify Java source file is the same as the JDK's
javadoc, which is reflected by the -subparckages
and -exclude options. The -exceptionOnError flag is
used by the Ant tasks, and -useFacadeInterface does not apply to
the ASTestGen tool.
Using R1-RPC/J Code Generation Ant Tasks
The R1-RPC/J Toolkit includes good Ant build script support. It contains
Ant tasks, <java2as> and <astestgen>,
for the two code generation tools, as well as Ant tasks for general
Adobe® Flex™ application compilation and deploying.
The included general Flex™ Ant tasks
include <mxmlc>, <compc>,
<asdoc>, and <htmlwrapper>.
We believe our tasks are advantageous over other similar tools on the market,
because they all support Ant's <fileset> data type, i.e.,
you can specify what source files to include and to exclude. Besides, the
tasks just use Flex™ SDK tool's command-line arguments and add very
few extra, except when it is really desired. For more detailed information
reguarding these general Ant tasks, please read ${ant.TOOLS_REF_link}.
All the Ant tasks work with Ant version 1.6.5 or higher. First, copy
r1tools.jar and the Velocity template engine library jar files
into the lib/ directory of your Ant installation. In your Ant
build scripts (usually named build.xml), add a line like this:
<taskdef resource="ant.tasks" classpath="r1tools.jar" />
| Task Attribute | For Task(s) | Description |
|---|---|---|
srcBase |
both | base directory for Java source files. |
destBase |
both | base directory for generated AS3 source files. |
packages |
both | package names separated by colon; all sub-packages are included. |
verbose |
both | a boolean, indicating to display more information in the process. |
useFacadeInterface |
java2as | if true, the generated facade class will implment a generate interface. |
testAppTitle |
astestgen | the RPC unit test application's title. |
rpcUri |
astestgen | the RPC URI used in the RPC unit test application. |
<fileset> |
both | The source files to include. |
Table 4. R1-RPC/J Ant Task Attributes
An example of client code generation is like this:
<java2as destBase="${build.client.src.dir}">
<fileset dir="${server.src.dir}">
<include name="**/*.java" />
</fileset>
</java2as>
The following is an example of RPC unit test case generation:
<astestgen testAppTitle="${test.webapp.title}" rpcUri="../rpc"
destBase="${build.client.src.dir}">
<fileset dir="${server.src.dir}">
<include name="**/*.java" />
</fileset>
</astestgen>
All the samples in the R1-RPC/J distribution have a build.xml.
They are mostly the same with minor changes, for instance, some scripts do
not build the unit test application by default. The best way to start a build
project is to find a sample that is closest in nature to your project, and
clone the build environment for your purpose.