### Eclipse Workspace Patch 1.0
#P moodle-HEAD
Index: webservice/amf/locallib.php
===================================================================
RCS file: /cvsroot/moodle/moodle/webservice/amf/locallib.php,v
retrieving revision 1.2
diff -u -r1.2 locallib.php
--- webservice/amf/locallib.php 11 Nov 2009 15:15:24 -0000 1.2
+++ webservice/amf/locallib.php 10 Feb 2010 07:58:37 -0000
@@ -24,6 +24,21 @@
*/
require_once("$CFG->dirroot/webservice/lib.php");
+require_once( "{$CFG->dirroot}/webservice/amf/introspector.php");
+
+/**
+ * Exception indicating an invalid return value from a function.
+ * Used when an externallib function does not return values of the expected structure.
+ */
+class invalid_return_value_exception extends moodle_exception {
+ /**
+ * Constructor
+ * @param string $debuginfo some detailed information
+ */
+ function __construct($debuginfo=null) {
+ parent::__construct('invalidreturnvalue', 'debug', '', null, $debuginfo);
+ }
+}
/**
* AMF service server implementation.
@@ -39,7 +54,108 @@
parent::__construct($simple, 'Zend_Amf_Server');
$this->wsname = 'amf';
}
+ protected function init_service_class(){
+ parent::init_service_class();
+ //allow access to data about methods available.
+ $this->zend_server->setClass( "MethodDescriptor" );
+ MethodDescriptor::$classnametointrospect = $this->service_class;
+ }
+
+ protected function service_class_method_body($function, $params){
+ $params = "webservice_amf_server::cast_objects_to_array($params)";
+ $externallibcall = $function->classname.'::'.$function->methodname.'('.$params.')';
+ $descriptionmethod = $function->methodname.'_returns()';
+ $callforreturnvaluedesc = $function->classname.'::'.$descriptionmethod;
+ return
+' return webservice_amf_server::validate_and_cast_values('.$callforreturnvaluedesc.', '.$externallibcall.', true)';
+ }
+ /**
+ * Validates submitted value, comparing it to a description. If anything is incorrect
+ * invalid_return_value_exception is thrown. Also casts the values to the type specified in
+ * the description.
+ * @param external_description $description description of parameters
+ * @param mixed $value the actual values
+ * @param boolean $singleasobject specifies whether a external_single_structure should be cast to a stdClass object
+ * should always be false for use in validating parameters in externallib functions.
+ * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found
+ */
+ public static function validate_and_cast_values(external_description $description, $value) {
+ if (is_null($description)){
+ return $value;
+ }
+ if ($description instanceof external_value) {
+ if (is_array($value) or is_object($value)) {
+ throw new invalid_return_value_exception('Scalar type expected, array or object received.');
+ }
+
+ if ($description->type == PARAM_BOOL) {
+ // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
+ if (is_bool($value) or $value === 0 or $value === 1 or $value === '0' or $value === '1') {
+ return (bool)$value;
+ }
+ }
+ return validate_param($value, $description->type, $description->allownull, 'Invalid external api parameter');
+ } else if ($description instanceof external_single_structure) {
+ if (!is_array($value)) {
+ throw new invalid_return_value_exception('Only arrays accepted.');
+ }
+ $result = array();
+ foreach ($description->keys as $key=>$subdesc) {
+ if (!array_key_exists($key, $value)) {
+ if ($subdesc->required == VALUE_REQUIRED) {
+ throw new invalid_return_value_exception('Missing required key in single structure: '.$key);
+ }
+ if ($subdesc instanceof external_value) {
+ if ($subdesc->required == VALUE_DEFAULT) {
+ $result[$key] = self::validate_and_cast_values($subdesc, $subdesc->default);
+ }
+ }
+ } else {
+ $result[$key] = self::validate_and_cast_values($subdesc, $value[$key]);
+ }
+ unset($value[$key]);
+ }
+ if (!empty($value)) {
+ throw new invalid_return_value_exception('Unexpected keys detected in parameter array.');
+ }
+ return (object)$result;
+
+ } else if ($description instanceof external_multiple_structure) {
+ if (!is_array($value)) {
+ throw new invalid_return_value_exception('Only arrays accepted.');
+ }
+ $result = array();
+ foreach ($value as $param) {
+ $result[] = self::validate_and_cast_values($description->content, $param);
+ }
+ return $result;
+
+ } else {
+ throw new invalid_return_value_exception('Invalid external api description.');
+ }
+ }
+ /**
+ * Recursive function to recurse down into a complex variable and convert all
+ * objects to arrays. Doesn't recurse down into objects or cast objects other than stdClass
+ * which is represented in Flash / Flex as an object.
+ * @param mixed $params value to cast
+ * @return mixed Cast value
+ */
+ public static function cast_objects_to_array($params){
+ if ($params instanceof stdClass){
+ $params = (array)$params;
+ }
+ if (is_array($params)){
+ $toreturn = array();
+ foreach ($params as $key=> $param){
+ $toreturn[$key] = self::cast_objects_to_array($param);
+ }
+ return $toreturn;
+ } else {
+ return $params;
+ }
+ }
/**
* Set up zend service class
* @return void
@@ -50,6 +166,8 @@
//(complete error message displayed into your AMF client)
// TODO: add some exception handling
}
+
+
}
// TODO: implement AMF test client somehow, maybe we could use moodle form to feed the data to the flash app somehow
Index: webservice/amf/testclient/index.php
===================================================================
RCS file: /cvsroot/moodle/moodle/webservice/amf/testclient/index.php,v
retrieving revision 1.7
diff -u -r1.7 index.php
--- webservice/amf/testclient/index.php 6 Feb 2010 12:41:09 -0000 1.7
+++ webservice/amf/testclient/index.php 10 Feb 2010 07:58:37 -0000
@@ -1,20 +1,23 @@
wwwroot.'/webservice/amf/testclient/moodleclient.swf';
-$args['width'] = '100%';
-$args['height'] = 500;
-$args['majorversion'] = 9;
-$args['build'] = 0;
-$args['allowscriptaccess'] = 'never';
-$args['quality'] = 'high';
-$args['flashvars'] = 'amfurl='.$CFG->wwwroot.'/webservice/amf/server.php';
-$args['setcontainercss'] = 'true';
+$flashvars = new object();
+$flashvars->rooturl =$CFG->wwwroot;
-$PAGE->requires->js('/lib/ufo.js');
-$PAGE->requires->js_function_call('M.util.create_UFO_object', array('moodletestclient', $args));
+
+$PAGE->requires->js('/lib/swfobject/swfobject.js', true);
+
+$PAGE->requires->js_function_call('swfobject.embedSWF',
+ array($CFG->wwwroot.'/webservice/amf/testclient/AMFTester.swf', //movie
+ 'moodletestclient', // div id
+ '100%', // width
+ '1000', // height
+ '9.0', // version
+ false,//no express install swf
+ $flashvars), //flash vars
+ true
+ );
$PAGE->set_title('Test Client');
$PAGE->set_heading('Test Client');
Index: webservice/lib.php
===================================================================
RCS file: /cvsroot/moodle/moodle/webservice/lib.php,v
retrieving revision 1.47
diff -u -r1.47 lib.php
--- webservice/lib.php 1 Feb 2010 03:38:28 -0000 1.47
+++ webservice/lib.php 10 Feb 2010 07:58:36 -0000
@@ -408,6 +408,8 @@
}
$params = implode(', ', $params);
$params_desc = implode("\n", $params_desc);
+
+ $serviceclassmethodbody = $this->service_class_method_body($function, $params);
if (is_null($function->returns_desc)) {
$return = ' * @return void';
@@ -441,12 +443,24 @@
'.$return.'
*/
public function '.$function->name.'('.$params.') {
- return '.$function->classname.'::'.$function->methodname.'('.$params.');
+'.$serviceclassmethodbody.';
}
';
return $code;
}
-
+
+ /**
+ * You can override this function in your child class to add extra code into the dynamically
+ * created service class. For example it is used in the amf server to cast types of parameters and to
+ * cast the return value to the types as specified in the return value description.
+ * @param unknown_type $function
+ * @param unknown_type $params
+ * @return string body of the method for $function ie. everything within the {} of the method declaration.
+ */
+ protected function service_class_method_body($function, $params){
+ return ' return '.$function->classname.'::'.$function->methodname.'('.$params.')';
+ }
+
/**
* Set up zend service class
* @return void
Index: webservice/amf/testclient/AMFTester.mxml
===================================================================
RCS file: webservice/amf/testclient/AMFTester.mxml
diff -N webservice/amf/testclient/AMFTester.mxml
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ webservice/amf/testclient/AMFTester.mxml 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,396 @@
+
+
+
+
+
+ */
+
+ import mx.controls.Label;
+ import mx.controls.Alert;
+ import mx.messaging.channels.AMFChannel;
+ import com.adobe.serialization.json.JSON;
+
+/* // Import the debugger
+ import nl.demonsters.debugger.MonsterDebugger;
+ */
+ public var api:AMFConnector;
+ protected var methods:Array;
+ protected var introspector:String;
+
+ public var rooturl:String;
+
+ [Bindable]
+ public var argumentToolTip:String = "You can use JSON syntax for method arguments ie. an array is written like this [item1, item2, etc.] objects are written {\"propname\":value, \"propname2\":value2, etc}";
+
+ // Variable to hold the debugger
+// private var debugger:MonsterDebugger;
+
+ /**
+ * restores the last settings if available
+ */
+ public function init():void
+ {
+ // Init the debugger
+// debugger = new MonsterDebugger(this);
+
+ // Send a simple trace
+// MonsterDebugger.trace(this, "Hello World!");
+
+ var so:SharedObject = SharedObject.getLocal('AMFTester');
+ if (so.data.token) {
+ token.text = so.data.token;
+ }
+ if (so.data.username) {
+ username.text = so.data.username;
+ password.text = so.data.password;
+ }
+ if (so.data.mode == 'username'){
+ loginType.selectedIndex = 1;
+ }
+ this.rememberpassword.selected = so.data.rememberpassword;
+ this.remembertoken.selected = so.data.remembertoken;
+ this.rooturl = Application.application.parameters.rooturl;
+ this.urllabel1.text = 'Root URL :'+this.rooturl;
+ this.urllabel2.text = 'Root URL :'+this.rooturl;
+
+ }
+ public function doConnectToken():void
+ {
+ var url:String = this.rooturl + '/webservice/amf/server.php?'+
+ 'wstoken='+this.token.text;
+ this.doConnect(url);
+ // saving settings for next time
+ var so:SharedObject = SharedObject.getLocal('AMFTester');
+ if (this.rememberpassword.selected == true ){
+ so.setProperty('token', this.token.text);
+ } else {
+ so.setProperty('token', null);//delete shared obj prop
+ }
+ so.setProperty('remembertoken', this.remembertoken.selected);
+ so.setProperty('mode', 'token');
+ so.flush();
+ }
+ public function doConnectUsername():void
+ {
+ var url:String = this.rooturl + '/webservice/amf/simpleserver.php?'+
+ 'wsusername=' + this.username.text+
+ '&wspassword=' + this.password.text;
+ this.doConnect(url);
+ // saving settings for next time
+ var so:SharedObject = SharedObject.getLocal('AMFTester');
+ if (this.rememberpassword.selected == true ){
+ so.setProperty('username', this.username.text);
+ so.setProperty('password', this.password.text);
+ } else {
+ so.setProperty('username', null);//delete shared obj prop
+ so.setProperty('password', null);
+ }
+ so.setProperty('rememberpassword', this.rememberpassword.selected);
+ so.setProperty('mode', 'username');
+ so.flush();
+ }
+
+ /**
+ * initializes the connection
+ */
+ private function doConnect(url:String):void
+ {
+ api = new AMFConnector(url);
+ api.exec('MethodDescriptor.getMethods');
+ api.addEventListener(Event.COMPLETE, handleConnection);
+ if (!api.hasEventListener(NetStatusEvent.NET_STATUS)) {
+ api.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
+ api.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
+ api.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
+ }
+ this.panelDebug.enabled = false;
+ }
+
+ /**
+ * initializes the debugger dialog with the method list and everything
+ */
+ protected function handleConnection(event:Event):void
+ {
+ methods = [];
+ for (var cls:String in api.data) {
+ for (var meth:String in api.data[cls]['methods']) {
+ methods.push({label: cls+'.'+meth, docs: api.data[cls]['methods'][meth]['docs'], args: api.data[cls]['methods'][meth]['params']});
+ }
+ }
+
+ this.panelDebug.enabled = true;
+ this.maintabs.selectedIndex = 1;
+ func.dataProvider = methods;
+ api.removeEventListener(Event.COMPLETE, handleConnection);
+ api.addEventListener(Event.COMPLETE, process);
+ reloadArgs();
+
+ }
+
+
+ /**
+ * outputs a response from the server
+ */
+ protected function process(event:Event):void
+ {
+ if (api.error) {
+ push(input, time() + ": Exception (code: "+api.data.code+", description: "+api.data.description+", detail: "+api.data.detail+", line: "+api.data.line+")\n");
+ } else {
+ push(input, time() + ": "+JSON.encode(api.data)+"\n");
+ }
+// MonsterDebugger.trace(this, api.data);
+ }
+
+ /**
+ * updates the display of arguments when the selected method changes
+ *
+ * it's hardly optimal to do it that way but it was faster to copy paste, I just hope nobody needs more than 7 args
+ */
+ protected function reloadArgs():void
+ {
+ var i:int;
+ for (i = 1; i <= 7; i++) {
+ this['arg'+i].visible = false;
+ this['arg'+i].includeInLayout = false;
+ this['larg'+i].visible = false;
+ this['larg'+i].includeInLayout = false;
+ this['JSONV'+i].enabled = false;
+ }
+ i = 1;
+ for (var arg:String in func.selectedItem.args) {
+ (this['arg'+i] as TextInput).visible = true;
+ (this['arg'+i] as TextInput).includeInLayout = true;
+ (this['larg'+i] as Label).visible = true;
+ (this['larg'+i] as Label).includeInLayout = true;
+ this['JSONV'+i].enabled = true;
+ this['JSONV'+i].required = func.selectedItem.args[arg]['required'];
+
+ (this['larg'+i++] as Label).text = func.selectedItem.args[arg]['name'] + (func.selectedItem.args[arg]['required'] ? "*":"");
+ }
+ if (func.selectedItem.docs == ""){
+ (this.methodDescription as TextArea).text = "";
+ (this.methodDescription as TextArea).visible = false;
+ (this.methodDescription as TextArea).includeInLayout = false;
+ } else {
+ (this.methodDescription as TextArea).text = func.selectedItem.docs.replace(/[\n\r\f]+/g, "\n");
+ (this.methodDescription as TextArea).visible = true;
+ (this.methodDescription as TextArea).includeInLayout = true;
+ }
+ }
+
+ /**
+ * calls a method on the server
+ */
+ protected function execute():void
+ {
+ var input:TextInput;
+ var argumentArray:Array = [];
+ var argumentErrors:Array = Validator.validateAll(argumentValidators);
+ if (argumentErrors.length != 0){
+// MonsterDebugger.trace(this, argumentErrors);
+ return;
+ }
+ for(var i:int = 1; i < 8; i++)
+ {
+ input = this['arg' +i] as TextInput;
+ if(input)
+ {
+ if (input.text.indexOf("{") == 0 || input.text.indexOf("[") == 0)
+ try {
+ argumentArray.push(JSON.decode(input.text));
+ } catch (err:Error){
+ return;
+ }
+ else
+ argumentArray.push(input.text as String);
+ }
+ }
+
+
+ api.exec(func.selectedLabel, argumentArray[0], argumentArray[1], argumentArray[2], argumentArray[3], argumentArray[4], argumentArray[5], argumentArray[6]);
+// MonsterDebugger.trace(this, [func.selectedLabel, argumentArray[0], argumentArray[1], argumentArray[2], argumentArray[3], argumentArray[4], argumentArray[5], argumentArray[6]]);
+ push(output, time() + ": Calling "+func.selectedLabel+" with arguments - "+JSON.encode(argumentArray));
+ }
+
+ /**
+ * clears debug consoles
+ */
+ protected function clear():void
+ {
+ input.text = output.text = "";
+ }
+
+ /**
+ * refreshes the method list
+ */
+ protected function refresh():void
+ {
+ api.removeEventListener(Event.COMPLETE, process);
+ api.addEventListener(Event.COMPLETE, handleConnection);
+ api.exec(introspector);
+ }
+
+ /**
+ * returns timestamp string
+ */
+ protected function time():String
+ {
+ var d:Date = new Date();
+ var ret:String = d.hours+":"+d.minutes+":"+d.seconds+"."+d.milliseconds;
+ return ret + "000000000000".substring(ret.length);
+ }
+
+ /**
+ * handler for specific net events
+ */
+ public function netStatusHandler(event:NetStatusEvent):void
+ {
+ push(input, time() + ": Error("+event.type+"): "+event.info.code+", "+event.info.description+", "+event.info.details);
+ }
+
+ /**
+ * handler for security errors
+ */
+ public function securityErrorHandler(event:SecurityErrorEvent):void
+ {
+ push(input, time() + ": Error("+event.type+"): "+event.text);
+ }
+
+ /**
+ * handler for io errors
+ */
+ public function ioErrorHandler(event:IOErrorEvent):void
+ {
+ push(input, time() + ": Error("+event.type+"): "+event.text);
+ }
+
+ /**
+ * pushes text into a console and scrolls it down automatically
+ */
+ public function push(target:TextArea, text:String):void
+ {
+ target.text += text + "\n";
+ target.verticalScrollPosition = target.maxVerticalScrollPosition;
+ }
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Index: webservice/amf/introspector.php
===================================================================
RCS file: webservice/amf/introspector.php
diff -N webservice/amf/introspector.php
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ webservice/amf/introspector.php 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,107 @@
+.
+ *
+ * @package moodle
+ * @author Penny Leach
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL
+ * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com
+ *
+ * Introspection for amf - figures out where all the services are and
+ * returns a list of their available methods.
+ * Requires $CFG->amf_introspection = true for security.
+ */
+
+
+/**
+ * Provides a function to get details of methods available on another class.
+ * @author HP
+ *
+ */
+class MethodDescriptor {
+
+ private $methods;
+ private $classes;
+
+ static public $classnametointrospect;
+
+
+ public function __construct() {
+ $this->setup();
+ }
+
+ private function setup() {
+ global $CFG;
+ if (!empty($this->nothing)) {
+ return; // we've already tried, no classes.
+ }
+ if (!empty($this->classes)) { // we've already done it successfully.
+ return;
+ }
+ /*if (empty($CFG->amf_introspection)) {
+ throw new Exception(get_string('amfintrospectiondisabled', 'local'));
+ }*/
+
+ //just one class here, possibility for expansion in future
+ $classes = array(MethodDescriptor::$classnametointrospect);
+
+ $hugestructure = array();
+
+ foreach ($classes as $c) {
+ $r = new ReflectionClass($c);
+
+ if (!$methods = $r->getMethods()) {
+ continue;
+ }
+ $this->classes[] = $c;
+ $hugestructure[$c] = array('docs' => $r->getDocComment(), 'methods' => array());
+ foreach ($methods as $method) {
+ if (!$method->isPublic()) {
+ continue;
+ }
+ $params = array();
+ foreach ($method->getParameters() as $param) {
+ $params[] = array('name' => $param->getName(), 'required' => !$param->isOptional());
+ }
+ $hugestructure[$c]['methods'][$method->getName()] = array(
+ 'docs' => $method->getDocComment(),
+ 'params' => $params,
+ );
+ }
+ }
+ $this->methods = $hugestructure;
+ if (empty($this->classes)) {
+ $this->nothing = true;
+ }
+ }
+
+ public function getMethods() {
+ $this->setup();
+ return $this->methods;
+ }
+
+ public function getClasses() {
+ $this->setup();
+ return $this->classes;
+ }
+
+ public function isConnected() {
+ return true;
+ }
+}
+
Index: webservice/amf/testclient/AMFConnector.as
===================================================================
RCS file: webservice/amf/testclient/AMFConnector.as
diff -N webservice/amf/testclient/AMFConnector.as
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ webservice/amf/testclient/AMFConnector.as 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,63 @@
+package {
+
+ import flash.events.Event;
+ import flash.net.NetConnection;
+ import flash.net.Responder;
+
+ import nl.demonsters.debugger.MonsterDebugger;
+
+ /**
+ * Wrapper class for the NetConnection/Responder instances
+ *
+ * This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Public License, Version 2, as published by Sam Hocevar. See
+ * http://sam.zoy.org/wtfpl/COPYING for more details.
+ *
+ * @author Jordi Boggiano
+ */
+ public class AMFConnector extends NetConnection {
+ private var responder:Responder;
+ public var data:Object;
+ public var error:Boolean = false;
+
+ public function AMFConnector(url:String) {
+ responder = new Responder(onSuccess, onError);
+ connect(url);
+ }
+
+ /**
+ * executes a command on the remote server, passing all the given arguments along
+ */
+ public function exec(command:String, ... args:Array):void
+ {
+ if (!args) args = [];
+ args.unshift(responder);
+ args.unshift(command);
+ (call as Function).apply(this, args);
+ }
+
+ /**
+ * handles success
+ */
+ protected function onSuccess(result:Object):void {
+ MonsterDebugger.trace(this, {'result':result});
+ data = result;
+ dispatchEvent(new Event(Event.COMPLETE));
+ data = null;
+ }
+
+ /**
+ * handles errors
+ */
+ protected function onError(result:Object):void {
+ data = result;
+ MonsterDebugger.trace(this, {'result':result});
+ error = true;
+ dispatchEvent(new Event(Event.COMPLETE));
+ error = false;
+ data = null;
+ }
+ }
+}
\ No newline at end of file
Index: webservice/amf/testclient/customValidators/JSONValidator.as
===================================================================
RCS file: webservice/amf/testclient/customValidators/JSONValidator.as
diff -N webservice/amf/testclient/customValidators/JSONValidator.as
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ webservice/amf/testclient/customValidators/JSONValidator.as 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,40 @@
+package customValidators
+{
+ import com.adobe.serialization.json.JSON;
+ import com.adobe.serialization.json.JSONParseError;
+
+ import mx.validators.ValidationResult;
+ import mx.validators.Validator;
+
+ import nl.demonsters.debugger.MonsterDebugger;
+
+ public class JSONValidator extends Validator
+ {
+ // Define Array for the return value of doValidation().
+ private var errors:Array;
+
+ public function JSONValidator()
+ {
+ super();
+ }
+
+ override protected function doValidation(value:Object):Array {
+ var JSONstring:String = String(value);
+ errors = [];
+ if (JSONstring != ''){
+ try {
+ JSON.decode(JSONstring);
+ } catch (err:Error){
+ errors.push(new ValidationResult(true, null, "JSON decode failed",
+ "Not able to decode this JSON."));
+ }
+ }
+ if (this.required && JSONstring == ''){
+ errors.push(new ValidationResult(true, null, "Required",
+ "You must enter a value for this argument."));
+ }
+ return errors;
+ }
+
+ }
+}
\ No newline at end of file
Index: webservice/amf/testclient/flashcompilationinstructions.txt
===================================================================
RCS file: webservice/amf/testclient/flashcompilationinstructions.txt
diff -N webservice/amf/testclient/flashcompilationinstructions.txt
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ webservice/amf/testclient/flashcompilationinstructions.txt 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+AMFTester.mxml can be compiled as a Flex application in Flex builder or using the Flex SDK.
+
+Copy the following into a Flex project source folder :
+
+* customValidators folder and contents
+* AMFConnector.as
+* AMFTester.mxml
+
+Then you need to use either the compiled Flex library or the source for the open source library as3corelib available here : http://code.google.com/p/as3corelib/downloads/list