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 }