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.event.distributor.EventDistributorControl;
    18  import org.as2lib.env.event.distributor.SimpleEventDistributorControl;
    19  import org.as2lib.env.except.IllegalArgumentException;
    20  import org.as2lib.io.conn.core.server.AbstractServerServiceProxy;
    21  import org.as2lib.io.conn.core.server.ServerServiceProxy;
    22  import org.as2lib.io.conn.core.server.ReservedServiceException;
    23  import org.as2lib.io.conn.core.event.MethodInvocationErrorListener;
    24  import org.as2lib.io.conn.core.event.MethodInvocationErrorInfo;
    25  import org.as2lib.io.conn.local.core.EnhancedLocalConnection;
    26  
    27  /**
    28   * {@code LocalServerServiceProxy} handles client requests to a certain service and
    29   * its responses.
    30   * 
    31   * <p>This client requests normally come from a client service proxy because this
    32   * class is designed to interact with this type of client.
    33   *
    34   * <p>You can setup your service proxy as follows to await client requests:
    35   * <code>
    36   *   var service:LocalServerServiceProxy = new LocalServerServiceProxy("myService", new MyService());
    37   *   service.run();
    38   * </code>
    39   * 
    40   * <p>A client may then invoke a method on this service proxy.
    41   * <code>
    42   *   var client = new LocalClientServiceProxy("myService");
    43   *   var callback:MethodInvocationCallback = client.myMethod("firstArgument", "secondArgument");
    44   * </code>
    45   * 
    46   * <p>You may choose to combine multiple services in one server for easier usage.
    47   * <code>
    48   *   var server:LocalServer = new LocalServer("local.as2lib.org");
    49   *   server.addService(new LocalServerServiceProxy("myServiceOne", new MyServiceOne()));
    50   *   server.addService(new LocalServerServiceProxy("myServiceTwo", new MyServiceTwo()));
    51   *   server.run();
    52   * </code>
    53   *
    54   * <p>A client must then prefix the service's name with the host of the server.
    55   * <code>
    56   *   var client = new LocalClientServiceProxy("local.as2lib.org/myService");
    57   *   var callback:MethodInvocationCallback = client.myMethod("firstArgument", "secondArgument");
    58   * </code>
    59   * 
    60   * <p>If the client invokes a method with arguments on this service that are not of
    61   * type {@code Number}, {@code String}, {@code Boolean} or {@code Array} which are
    62   * converted dynamically to the correct type, Flash just creates a new object of type
    63   * {@code Object} and populates it with the instance variables of the passed object.
    64   * To receive an instance of correct type you must thus register the class. Note
    65   * that the client must register the same class with the same name. This registration
    66   * must also be done for return values on the client and the server.
    67   * <code>
    68   *   Object.registerClass("MyClass", MyClass);
    69   * </code>
    70   * 
    71   * <p>The received object will now be of correct type. But you still have to be aware
    72   * of some facts.<br>
    73   * Flash creates a new object in the background and sets the instance variables of
    74   * the sent instance to the new object. It then registers this object to the appropriate
    75   * class (if registered previously) and applies the constructor of that class to the
    76   * new object passing no arguments. This means if the constructor sets instance variables
    77   * it overwrites the ones set previously by {@code undefined}.
    78   *
    79   * @author Simon Wacker
    80   * @author Christoph Atteneder
    81   */
    82  class org.as2lib.io.conn.local.server.LocalServerServiceProxy extends AbstractServerServiceProxy implements ServerServiceProxy {
    83  	
    84  	/** The wrapped service object. */
    85  	private var service;
    86  	
    87  	/** The service path. */
    88  	private var path:String;
    89  	
    90  	/** Used to run this service. */
    91  	private var connection:EnhancedLocalConnection;
    92  	
    93  	/** Stores set error listeners and controls the {@code errorDistributor}. */
    94  	private var errorDistributorControl:EventDistributorControl;
    95  	
    96  	/** Distributes to all added error listeners. */
    97  	private var errorDistributor:MethodInvocationErrorListener;
    98  	
    99  	/** This service's status. */
   100  	private var running:Boolean;
   101  	
   102  	/** Stores the current service url. */
   103  	private var currentServiceUrl:String;
   104  	
   105  	/**
   106  	 * Constructs a new {@code LocalServerServiceProxy} instance.
   107  	 * 
   108  	 * @param path the path of this service
   109  	 * @param service object that provides the service's operations
   110  	 * @throws IllegalArgumentException if {@code path} is {@code null}, {@code undefined}
   111  	 * or an empty string or if {@code service} is {@code null} or {@code undefined}
   112  	 */
   113  	public function LocalServerServiceProxy(path:String, service) {
   114  		if (!path || !service) throw new IllegalArgumentException("Neither the path [" + path + "] nor the service [" + service + "] are allowed to be null, undefined or a blank string.", this, arguments);
   115  		this.path = path;
   116  		this.service = service;
   117  		running = false;
   118  		currentServiceUrl = null;
   119  		errorDistributorControl = new SimpleEventDistributorControl(MethodInvocationErrorListener, false);
   120  		errorDistributor = errorDistributorControl.getDistributor();
   121  	}
   122  	
   123  	/**
   124  	 * Returns the currently used connection.
   125  	 *
   126  	 * <p>This is either the connection set via the {@link #setConnection} method or
   127  	 * the default one which is an instance of class {@link EnhancedLocalConnection}.
   128  	 * 
   129  	 * @return the currently used connection
   130  	 */
   131  	public function getConnection(Void):EnhancedLocalConnection {
   132  		if (!connection) connection = new EnhancedLocalConnection(this);
   133  		return connection;
   134  	}
   135  	
   136  	/**
   137  	 * Sets a new connection.
   138  	 *
   139  	 * <p>If {@code connection} is {@code null} or {@code undefined}, {@link #getConnection}
   140  	 * will return the default connection.
   141  	 * 
   142  	 * @param connection the new connection
   143  	 */
   144  	public function setConnection(connection:EnhancedLocalConnection):Void {
   145  		this.connection = connection;
   146  	}
   147  	
   148  	/**
   149  	 * Runs this service proxy on the passed-in {@code host}.
   150  	 *
   151  	 * <p>This service proxy will be restarted if it is already running. This means it
   152  	 * it first stops itself and starts itself again.
   153  	 * 
   154  	 * <p>Only the path of this service proxy is used to connect if the passed-in {@code host}
   155  	 * is {@code null}, {@code undefined} or an empty string.
   156  	 * 
   157  	 * @param host the host to run the service on
   158  	 * @throws ReservedServiceException if a service on the passed-in {@code host} with
   159  	 * the service's path is already in use
   160  	 */
   161  	public function run(host:String):Void {
   162  		if (isRunning()) this.stop();
   163  		try {
   164  			currentServiceUrl = generateServiceUrl(host, path);
   165  			getConnection().connect(currentServiceUrl);
   166  			running = true;
   167  		} catch(exception:org.as2lib.io.conn.local.core.ReservedConnectionException) {
   168  			// "new ReservedServiceException" without braces is not MTASC compatible
   169  			throw (new ReservedServiceException("Service with url [" + currentServiceUrl + "] is already in use.", this, arguments)).initCause(exception);
   170  		}
   171  	}
   172  	
   173  	/**
   174  	 * Stops this service.
   175  	 */
   176  	public function stop(Void):Void {
   177  		getConnection().close();
   178  		running = false;
   179  		currentServiceUrl = null;
   180  	}
   181  	
   182  	/**
   183  	 * Handles incoming 'remote' method invocations on the service.
   184  	 * 
   185  	 * <p>The method corresponding to the passed-in {@code methodName} is invoked on the
   186  	 * wrapped service.
   187  	 *
   188  	 * <p>The error listeners will be informed of a failure if:
   189  	 * <ul>
   190  	 *   <li>
   191  	 *     A method with the passed-in {@code methodName} does not exist on the wrapped
   192  	 *     service.
   193  	 *   </li>
   194  	 *   <li>The service method threw an exception.</li>
   195  	 * </ul>
   196  	 * 
   197  	 * @param methodName the name of the method to invoke on the service
   198  	 * @param args the arguments to use as parameters when invoking the method
   199  	 */
   200  	public function invokeMethodByNameAndArguments(methodName:String, args:Array):Void {
   201  		try {
   202  			if (service[methodName]) {
   203  				service[methodName].apply(service, args);
   204  			} else {
   205  				errorDistributor.onError(new MethodInvocationErrorInfo(currentServiceUrl, methodName, args, MethodInvocationErrorInfo.UNKNOWN_METHOD_ERROR, null));
   206  			}
   207  		} catch (exception) {
   208  			errorDistributor.onError(new MethodInvocationErrorInfo(currentServiceUrl, methodName, args, MethodInvocationErrorInfo.METHOD_EXCEPTION_ERROR, exception));
   209  		}
   210  	}
   211  	
   212  	/**
   213  	 * Handles incoming 'remote' method invocations on the service and responses through
   214  	 * the {@code responseServiceUrl}.
   215  	 * 
   216  	 * <p>The method corresponding to the passed-in {@code methodName} is invoked on the
   217  	 * service and the response of this invocation is passed through the
   218  	 * {@code responseServiceUrl} to the client.
   219  	 *
   220  	 * <p>If the response service url is {@code null} or an empty string the 
   221  	 * {@link #invokeMethodByNameAndArguments} method is invoked instead.
   222  	 * 
   223  	 * <p>The response service is supposed to implement two methods with the following
   224  	 * signature:
   225  	 * <ul>
   226  	 *   <li>onReturn(returnValue):Void</li>
   227  	 *   <li>onError(errorCode:Number, exception):Void</li>
   228  	 * </ul>
   229  	 * 
   230  	 * <p>The {@code onReturn} method is invoked on the response service if the method
   231  	 * returned successfully.
   232  	 * 
   233  	 * <p>The {@code onError} method is invoked on the response service if:
   234  	 * <ul>
   235  	 *   <li>The method threw an exception.</li>
   236  	 *   <li>The method does not exist on the service.</li>
   237  	 * </ul>
   238  	 *
   239  	 * <p>The error listeners will be informed of a failure if:
   240  	 * <ul>
   241  	 *   <li>
   242  	 *     A method with the passed-in {@code methodName} does not exist on the wrapped
   243  	 *     service.
   244  	 *   </li>
   245  	 *   <li>The service method threw an exception.</li>
   246  	 *   <li>
   247  	 *     The response server with the given {@code responseServiceUrl} does not exist.
   248  	 *   </li>
   249  	 *   <li>The return value is too big to send over a local connection.</li>
   250  	 *   <li>An unknown failure occured when trying to send the response.</li>
   251  	 * </ul>
   252  	 * 
   253  	 * @param methodName the name of the method to invoke on the service
   254  	 * @param args the arguments to use as parameters when invoking the method
   255  	 * @param responseServiceUrl the url to the service that handles the response
   256  	 */
   257  	public function invokeMethodByNameAndArgumentsAndResponseService(methodName:String, args:Array, responseServiceUrl:String):Void {
   258  		if (!responseServiceUrl) {
   259  			invokeMethodByNameAndArguments(methodName, args);
   260  			return;
   261  		}
   262  		var listener:MethodInvocationErrorListener = getBlankMethodInvocationErrorListener();
   263  		var owner:LocalServerServiceProxy = this;
   264  		listener.onError = function(info:MethodInvocationErrorInfo):Void {
   265  			// "owner.errorDistributor" and "owner.currentServiceUrl" are not MTASC compatible
   266  			owner["errorDistributor"].onError(new MethodInvocationErrorInfo(owner["currentServiceUrl"], methodName, args, MethodInvocationErrorInfo.UNKNOWN_ERROR, null));
   267  		};
   268  		try {
   269  			if (service[methodName]) {
   270  				var returnValue = service[methodName].apply(service, args);
   271  				sendResponse(methodName, args, responseServiceUrl, "onReturn", [returnValue], listener);
   272  			} else {
   273  				sendResponse(methodName, args, responseServiceUrl, "onError", [MethodInvocationErrorInfo.UNKNOWN_METHOD_ERROR, null], listener);
   274  				errorDistributor.onError(new MethodInvocationErrorInfo(currentServiceUrl, methodName, args, MethodInvocationErrorInfo.UNKNOWN_METHOD_ERROR, null));
   275  			}
   276  		} catch (serviceMethodException) {
   277  			sendResponse(methodName, args, responseServiceUrl, "onError", [MethodInvocationErrorInfo.METHOD_EXCEPTION_ERROR, serviceMethodException], listener);
   278  			errorDistributor.onError(new MethodInvocationErrorInfo(currentServiceUrl, methodName, args, MethodInvocationErrorInfo.METHOD_EXCEPTION_ERROR, serviceMethodException));
   279  		}
   280  	}
   281  	
   282  	/**
   283  	 * Returns a blank method invocation error listener. This is an error listern with
   284  	 * no implemented methods.
   285  	 * 
   286  	 * @return a blank method invocation error listener
   287  	 */
   288  	private function getBlankMethodInvocationErrorListener(Void):MethodInvocationErrorListener {
   289  		var result = new Object();
   290  		result.__proto__ = MethodInvocationErrorListener["prototype"];
   291  		result.__constructor__ = MethodInvocationErrorListener;
   292  		return result;
   293  	}
   294  	
   295  	/**
   296  	 * Sends a response to the client.
   297  	 * 
   298  	 * @param methodName the name of the method
   299  	 * @param methodArguments the arguments used for the method invocation
   300  	 * @param responseServiceUrl the url of the response service
   301  	 * @param responseMethod the response method to invoke on the response service
   302  	 * @param responseArguments the arguments to pass to the response method
   303  	 * @param responseListener the listener that listens for failures that may occur when
   304  	 * sending the response
   305  	 */
   306  	private function sendResponse(methodName:String, methodArguments:Array, responseServiceUrl:String, responseMethod:String, responseArguments:Array, responseListener:MethodInvocationErrorListener):Void {
   307  		try {
   308  			getConnection().send(responseServiceUrl, responseMethod, responseArguments, responseListener);
   309  		} catch (uce:org.as2lib.io.conn.local.core.UnknownConnectionException) {
   310  			errorDistributor.onError(new MethodInvocationErrorInfo(currentServiceUrl, methodName, methodArguments, MethodInvocationErrorInfo.UNKNOWN_SERVICE_ERROR, uce));
   311  		} catch (mie:org.as2lib.io.conn.core.client.MethodInvocationException) {
   312  			errorDistributor.onError(new MethodInvocationErrorInfo(currentServiceUrl, methodName, methodArguments, MethodInvocationErrorInfo.OVERSIZED_ARGUMENTS_ERROR, mie));
   313  		}
   314  	}
   315  	
   316  	/**
   317  	 * Returns the wrapped service.
   318  	 *
   319  	 * @return the wrapped service
   320  	 */
   321  	public function getService(Void) {
   322  		return service;
   323  	}
   324  	
   325  	/**
   326  	 * Returns the path of this service.
   327  	 */
   328  	public function getPath(Void):String {
   329  		return path;
   330  	}
   331  	
   332  	/**
   333  	 * Returns whether this service is running or not.
   334  	 *
   335  	 * @return {@code true} if this service is running else {@code false}
   336  	 */
   337  	public function isRunning(Void):Boolean {
   338  		return running;
   339  	}
   340  	
   341  	/**
   342  	 * Adds an error listener.
   343  	 * 
   344  	 * <p>Error listeners are notified when a client tried to invoke a method on this
   345  	 * service and something went wrong.
   346  	 * 
   347  	 * @param errorListener the new error listener to add
   348  	 */
   349  	public function addErrorListener(errorListener:MethodInvocationErrorListener):Void {
   350  		errorDistributorControl.addListener(errorListener);
   351  	}
   352  	
   353  	/**
   354  	 * Removes an error listener.
   355  	 *
   356  	 * <p>Error listeners are notified when a client tried to invoke a method on this
   357  	 * service and something went wrong.
   358  	 *
   359  	 * @param errorListener the error listener to remove
   360  	 */
   361  	public function removeErrorListener(errorListener:MethodInvocationErrorListener):Void {
   362  		errorDistributorControl.removeListener(errorListener);
   363  	}
   364  	
   365  }