1  /*
     2   * Copyright the original author or authors.
     3   * 
     4   * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   * 
     8   *      http://www.mozilla.org/MPL/MPL-1.1.html
     9   * 
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  import org.as2lib.env.except.IllegalArgumentException;
    18  import org.as2lib.io.conn.core.client.ClientServiceProxy;
    19  import org.as2lib.io.conn.core.client.AbstractClientServiceProxy;
    20  import org.as2lib.io.conn.core.client.UnknownServiceException;
    21  import org.as2lib.io.conn.core.server.ReservedServiceException;
    22  import org.as2lib.io.conn.core.event.MethodInvocationCallback;
    23  import org.as2lib.io.conn.core.event.MethodInvocationReturnInfo;
    24  import org.as2lib.io.conn.core.event.MethodInvocationErrorInfo;
    25  import org.as2lib.io.conn.core.event.MethodInvocationErrorListener;
    26  import org.as2lib.io.conn.local.core.EnhancedLocalConnection;
    27  
    28  /**
    29   * {@code LocalClientServiceProxy} handles client requests to a certain service
    30   * and its responses.
    31   * 
    32   * <p>Example:
    33   * <code>
    34   *   var client:LocalClientServiceProxy = new LocalClientServiceProxy("local.as2lib.org/myService");
    35   *   var callback:MethodInvocationCallback = client.invoke("myMethod", ["firstArgument", "secondArgument"]);
    36   *   callback.onReturn = function(returnInfo:MethodInvocationReturnInfo):Void {
    37   *       trace("myMethod - return value: " + returnInfo.getReturnValue());
    38   *   }
    39   *   callback.onError = function(errorInfo:MethodInvocationErrorInfo):Void {
    40   *       trace("myMethod - error: " + errorInfo.getException());
    41   *   }
    42   * </code>
    43   * 
    44   * <p>It is also possible to call the method directly on the proxy. But you can't
    45   * type the proxy then.
    46   * <code>
    47   *   var client = new LocalClientServiceProxy("local.as2lib.org/myService");
    48   *   var callback:MethodInvocationCallback = client.myMethod("firstArgument", "secondArgument");
    49   * </code>
    50   *
    51   * <p>The neatest way is to use {@code LocalClientServiceProxyFactory} to get a proxy
    52   * for a service interface or class, which enables compiler checks. For more
    53   * information on this refer to the {@link LocalClientServiceProxyFactory} class.
    54   * 
    55   * <p>If the return value is not of type {@code Number}, {@code Boolean}, {@code String}
    56   * or {@code Array} that are converted directly into the appropriate type you must
    57   * do the following to receive a value of correct type. Otherwise the return value
    58   * will be an instance of type Object that is populated with the instance variables
    59   * of the sent object. Note that this must be done on the client as well as on the
    60   * server and the 'symbolId' in this case {@code "MyClass"} must be the same.
    61   * <code>
    62   *   Object.registerClass("MyClass", MyClass);
    63   * </code>
    64   * 
    65   * <p>The received object will now be of correct type. But you still have to be aware
    66   * of some facts:<br>
    67   * Flash creates a new object in the background and sets the instance variables of
    68   * the sent instance to the new object. It then registers this object with the
    69   * appropriate class (if registered previously) and applies the constructor of that
    70   * class to the new object passing no arguments. This means if the constructor sets
    71   * instance variables it overwrites the ones set previously by {@code undefined}.
    72   *
    73   * @author Simon Wacker
    74   * @author Christoph Atteneder
    75   * @see org.as2lib.io.conn.local.client.LocalClientServiceProxyFactory
    76   */
    77  class org.as2lib.io.conn.local.client.LocalClientServiceProxy extends AbstractClientServiceProxy implements ClientServiceProxy {
    78  	
    79  	/**
    80  	 * Generates the response url for a service.
    81  	 * 
    82  	 * <p>The response url is composed as follows:
    83  	 * <pre>theServiceUrl.theMethodName_Return_theIndex</pre>
    84  	 * 
    85  	 * <p>If the passed-in {@code methodName} is {@code null}, {@code undefined} or an
    86  	 * empty string the response url will be composed as follows:
    87  	 * <pre>theServiceUrl_Return_theIndex</pre>
    88  	 *
    89  	 * <p>{@code index} is a number from 0 to infinite depending on how many responses
    90  	 * are pending.
    91  	 * 
    92  	 * @param serviceUrl the url to the service
    93  	 * @param methodName the name of the responsing method
    94  	 * @return the generated response url
    95  	 * @throws IllegalArgumentException if the passed-in {@code serviceUrl} is {@code null},
    96  	 * {@code undefined} or an empty stirng
    97  	 */
    98  	public static function generateResponseServiceUrl(serviceUrl:String, methodName:String):String {
    99  		if (!serviceUrl) throw new IllegalArgumentException("Service url must not be null, undefined or an empty string.", eval("th" + "is"), arguments);
   100  		if (!methodName) {
   101  			var result:String = serviceUrl + "_Return";
   102  			var i:Number = 0;
   103  			while (EnhancedLocalConnection.connectionExists(result + "_" + i)) {
   104  				i++;
   105  			}
   106  			return (result + "_" + i);
   107  		} else {
   108  			var result:String = serviceUrl + "_" + methodName + "_Return";
   109  			var i:Number = 0;
   110  			while (EnhancedLocalConnection.connectionExists(result + "_" + i)) {
   111  				i++;
   112  			}
   113  			return (result + "_" + i);
   114  		}
   115  	}
   116  	
   117  	/** The url of the service. */
   118  	private var url:String;
   119  	
   120  	/** Used EnhancedLocalConnection. */
   121  	private var connection:EnhancedLocalConnection;
   122  	
   123  	/** Stores all currently used response services. */
   124  	private var responseServices:Array;
   125  	
   126  	/**
   127  	 * Constructs a new {@code LocalClientServiceProxy} instance.
   128  	 * 
   129  	 * @param url the url of the service
   130  	 * @throws IllegalArgumentException if {@code url} is {@code null}, {@code undefined}
   131  	 * or an empty string
   132  	 */
   133  	public function LocalClientServiceProxy(url:String) {
   134  		if (!url) throw new IllegalArgumentException("Argument 'url' must not be null, undefined or an empty string.", this, arguments);
   135  		this.url = url;
   136  		connection = new EnhancedLocalConnection();
   137  		responseServices = new Array();
   138  	}
   139  	
   140  	/**
   141  	 * Returns the url of the service this proxy invokes methods on.
   142  	 * 
   143  	 * <p>The returned url is never {@code null}, {@code undefined} or an empty string.
   144  	 *
   145  	 * @return the url of the service this proxy invokes methods on
   146  	 */
   147  	public function getUrl(Void):String {
   148  		return url;
   149  	}
   150  	
   151  	/**
   152  	 * Invokes the method with passed-in {@code methodName} on the 'remote' service,
   153  	 * passing the elements of the passed-in {@code args} as parameters and invokes
   154  	 * the appropriate method on the passed-in {@code callback} on response.
   155  	 * 
   156  	 * <p>The response of the method invocation is delegated to the appropriate method
   157  	 * on the passed-in {@code callback}. This is either the {@code onReturn} when no
   158  	 * error occured, or the {@code onError} method in case something went wrong.
   159  	 *
   160  	 * <p>If the passed-in {@code callback} is {@code null} a new {@code MethodInvocationCallback}
   161  	 * instance will be created and returned. It is possible to still set the callback
   162  	 * methods there, after invoking this method.
   163  	 * 
   164  	 * @param methodName the name of the method to invoke on the 'remote' service
   165  	 * @param args the arguments that are passed to the method as parameters
   166  	 * @param callback the callback that handles the response
   167  	 * @return either the passed-in callback or a new callback if {@code null}
   168  	 * @throws IllegalArgumentException if the passed-in {@code methodName} is {@code null},
   169  	 * {@code undefined} or an empty string
   170  	 */
   171  	public function invokeByNameAndArgumentsAndCallback(methodName:String, args:Array, callback:MethodInvocationCallback):MethodInvocationCallback {
   172  		if (!methodName) throw new IllegalArgumentException("Argument 'methodName' must not be null, undefined or an empty string.", this, arguments);
   173  		if (!args) args = new Array();
   174  		if (!callback) callback = getBlankMethodInvocationCallback();
   175  		
   176  		var responseUrl:String = generateResponseServiceUrl(url, methodName);
   177  		
   178  		var responseService:EnhancedLocalConnection = new EnhancedLocalConnection();
   179  		var index:Number = responseServices.push(responseService) - 1;
   180  		var owner:LocalClientServiceProxy = this;
   181  		responseService["onReturn"] = function(returnValue):Void {
   182  			// "owner.responseServices" is not MTASC compatible because "responseServices" is private
   183  			owner["responseServices"].splice(index, 1);
   184  			// "owner.url" is not MTASC compatible because "url" is private
   185  			callback.onReturn(new MethodInvocationReturnInfo(owner["url"], methodName, args, returnValue));
   186  			this.close();
   187  		};
   188  		responseService["onError"] = function(errorCode:Number, exception):Void {
   189  			// "owner.responseServices" is not MTASC compatible because "responseServices" is private
   190  			owner["responseServices"].splice(index, 1);
   191  			// "owner.url" is not MTASC compatible because "url" is private
   192  			callback.onError(new MethodInvocationErrorInfo(owner["url"], methodName, args, errorCode, exception));
   193  			this.close();
   194  		};
   195  		try {
   196  			responseService.connect(responseUrl);
   197  		} catch (exception:org.as2lib.io.conn.local.core.ReservedConnectionException) {
   198  			// "new ReservedServiceException" without braces is not MTASC compatible
   199  			throw (new ReservedServiceException("Response service with url [" + responseUrl + "] does already exist.", this, arguments)).initCause(exception);
   200  		}
   201  		
   202  		var errorListener:MethodInvocationErrorListener = getBlankMethodInvocationErrorListener();
   203  		errorListener.onError = function(info:MethodInvocationErrorInfo) {
   204  			callback.onError(info);
   205  		};
   206  		
   207  		try {
   208  			connection.send(url, "invokeMethod", [methodName, args, responseUrl], errorListener);
   209  		} catch (exception:org.as2lib.io.conn.local.core.UnknownConnectionException) {
   210  			// "new UnknownServiceException" without braces is not MTASC compatible
   211  			throw (new UnknownServiceException("Service with url [" + url + "] does not exist.", this, arguments)).initCause(exception);
   212  		}
   213  		
   214  		return callback;
   215  	}
   216  	
   217  	/**
   218  	 * Returns a blank method invocation error listener. This is an error listern with
   219  	 * no implemented methods.
   220  	 * 
   221  	 * @return a blank method invocation error listener
   222  	 */
   223  	private function getBlankMethodInvocationErrorListener(Void):MethodInvocationErrorListener {
   224  		var result = new Object();
   225  		result.__proto__ = MethodInvocationErrorListener["prototype"];
   226  		result.__constructor__ = MethodInvocationErrorListener;
   227  		return result;
   228  	}
   229  	
   230  	/**
   231  	 * Returns a blank method invocation callback. This is a callback with no implemented
   232  	 * methods.
   233  	 * 
   234  	 * @return a blank method invocation callback
   235  	 */
   236  	private function getBlankMethodInvocationCallback(Void):MethodInvocationCallback {
   237  		var result = new Object();
   238  		result.__proto__ = MethodInvocationCallback["prototype"];
   239  		result.__constructor__ = MethodInvocationCallback;
   240  		return result;
   241  	}
   242  	
   243  	/**
   244  	 * Enables you to invoke the method to be invoked on the 'remote' service directly
   245  	 * on this proxy.
   246  	 * 
   247  	 * <p>The usage is mostly the same.
   248  	 * <code>myProxy.myMethod("myArg1");</code>
   249  	 * <code>myProxy.myMethod("myArg1", myCallback);</code>
   250  	 * <code>var callback:MethodInvocationCallback = myProxy.myMethod("myArg1");</code>
   251  	 * 
   252  	 * @param methodName the name of the method to invoke on the 'remote' service
   253  	 * @return the function to execute as the actual method passing the actual arguments
   254  	 */
   255  	private function __resolve(methodName:String):Function {
   256  		var owner:ClientServiceProxy = this;
   257  		return (function():MethodInvocationCallback {
   258  			if (arguments[arguments.length] instanceof MethodInvocationCallback) {
   259  				return owner.invokeByNameAndArgumentsAndCallback(methodName, arguments, MethodInvocationCallback(arguments.pop()));
   260  			} else {
   261  				return owner.invokeByNameAndArgumentsAndCallback(methodName, arguments, null);
   262  			}
   263  		});
   264  	}
   265  
   266  }