1 /** 2 * @fileOverview Exposes a set of API wrappers that will hide the dirty work of 3 * constructing Finesse API requests and consuming Finesse events. 4 * 5 * @name ClientServices 6 * @requires OpenAjax, jQuery 1.5, finesse.utilities.Utilities 7 */ 8 9 var finesse = finesse || {}; 10 finesse.version = '$version$'; 11 finesse.clientservices = finesse.clientservices || {}; 12 /** 13 * @class 14 * Allow clients to make Finesse API requests and consume Finesse events by 15 * calling a set of exposed functions. The Services layer will do the dirty 16 * work of establishing a shared BOSH connection (for designated Master 17 * modules), consuming events for client subscriptions, and constructing API 18 * requests. 19 */ 20 finesse.clientservices.ClientServices = (function () { 21 22 var 23 24 /** 25 * Shortcut reference to the finesse.utilities.Utilities singleton 26 * This will be set by init() 27 * @private 28 */ 29 _util, 30 31 /** 32 * Shortcut reference to the gadgets.io object. 33 * This will be set by init() 34 * @private 35 */ 36 _io, 37 38 /** 39 * Shortcut reference to the gadget pubsub Hub instance. 40 * This will be set by init() 41 * @private 42 */ 43 _hub, 44 45 /** 46 * Shortcut reference to the Topics class. 47 * This will be set by init() 48 * @private 49 */ 50 _topics, 51 52 /** 53 * Config object needed to initialize this library 54 * This must be set by init() 55 * @private 56 */ 57 _config, 58 59 /** 60 * @private 61 * Whether or not this ClientService instance is a Master. 62 */ 63 _isMaster = false, 64 65 /** 66 * @private 67 * Whether the Client Services have been initiated yet. 68 */ 69 _inited = false, 70 71 /** 72 * Stores the list of subscription IDs for all subscriptions so that it 73 * could be retrieve for unsubscriptions. 74 * @private 75 */ 76 _subscriptionID = {}, 77 78 /** 79 * The possible states of the JabberWerx BOSH connection. 80 * @private 81 */ 82 _STATUS = { 83 CONNECTING: "connecting", 84 CONNECTED: "connected", 85 DISCONNECTED: "disconnected", 86 DISCONNECTING: "disconnecting", 87 RECONNECTING: "reconnecting", 88 UNLOADING: "unloading" 89 }, 90 91 /** 92 * Handler function to be invoked when BOSH connection is connecting. 93 * @private 94 */ 95 _onConnectingHandler, 96 97 /** 98 * Handler function to be invoked when BOSH connection is connected 99 * @private 100 */ 101 _onConnectHandler, 102 103 /** 104 * Handler function to be invoked when BOSH connection is disconnecting. 105 * @private 106 */ 107 _onDisconnectingHandler, 108 109 /** 110 * Handler function to be invoked when the BOSH is disconnected. 111 * @private 112 */ 113 _onDisconnectHandler, 114 115 /** 116 * Handler function to be invoked when the BOSH is reconnecting. 117 * @private 118 */ 119 _onReconnectingHandler, 120 121 /** 122 * Handler function to be invoked when the BOSH is unloading. 123 * @private 124 */ 125 _onUnloadingHandler, 126 127 /** 128 * Contains a cache of the latest connection info containing the current 129 * state of the BOSH connection and the resource ID. 130 * @private 131 */ 132 _connInfo, 133 134 /** 135 * Handler to process connection info publishes. 136 * @param {Object} data 137 * The connection info data object. 138 * @param {String} data.status 139 * The BOSH connection status. 140 * @param {String} data.resourceID 141 * The resource ID for the connection. 142 * @private 143 */ 144 _connInfoHandler = function (data) { 145 146 //Invoke registered handler depending on status received. Due to the 147 //request topic where clients can make request for the Master to publish 148 //the connection info, there is a chance that duplicate connection info 149 //events may be sent, so ensure that there has been a state change 150 //before invoking the handlers. 151 if (_connInfo === undefined || _connInfo.status !== data.status) { 152 _connInfo = data; 153 switch (data.status) { 154 case _STATUS.CONNECTING: 155 if (_onConnectingHandler) { 156 _onConnectingHandler(); 157 } 158 break; 159 case _STATUS.CONNECTED: 160 if (_onConnectHandler) { 161 _onConnectHandler(); 162 } 163 break; 164 case _STATUS.DISCONNECTED: 165 if (_onDisconnectHandler) { 166 _onDisconnectHandler(); 167 } 168 break; 169 case _STATUS.DISCONNECTING: 170 if (_onDisconnectingHandler) { 171 _onDisconnectingHandler(); 172 } 173 break; 174 case _STATUS.RECONNECTING: 175 if (_onReconnectingHandler) { 176 _onReconnectingHandler(); 177 } 178 break; 179 case _STATUS.UNLOADING: 180 if (_onUnloadingHandler) { 181 _onUnloadingHandler(); 182 } 183 } 184 } 185 }, 186 187 /** 188 * Ensure that ClientServices have been inited. 189 * @private 190 */ 191 _isInited = function () { 192 if (!_inited) { 193 throw new Error("ClientServices needs to be inited."); 194 } 195 }, 196 197 /** 198 * Have the client become the Master by initiating a tunnel to a shared 199 * event BOSH connection. The Master is responsible for publishing all 200 * events to the pubsub infrastructure. 201 * @private 202 */ 203 _becomeMaster = function () { 204 var tunnel = new finesse.clientservices.MasterTunnel(_config.host), 205 publisher = new finesse.clientservices.MasterPublisher(tunnel); 206 tunnel.init(_config.id, _config.password, _config.xmppDomain, _config.pubsubDomain); 207 _isMaster = true; 208 }, 209 210 /** 211 * Make a request to the request channel to have the Master publish the 212 * connection info object. 213 * @private 214 */ 215 _makeConnectionInfoReq = function () { 216 var data = { 217 type: "ConnectionInfoReq", 218 data: {}, 219 invokeID: (new Date()).getTime() 220 }; 221 _hub.publish(_topics.REQUESTS, data); 222 }, 223 224 /** 225 * Utility method to register a handler which is associated with a 226 * particular connection status. 227 * @param {String} status 228 * The connection status string. 229 * @param {Function} handler 230 * The handler to associate with a particular connection status. 231 * @throws {Error} 232 * If the handler provided is not a function. 233 * @private 234 */ 235 _registerHandler = function (status, handler) { 236 if (typeof handler === "function") { 237 if (_connInfo && _connInfo.status === status) { 238 handler(); 239 } 240 switch (status) { 241 case _STATUS.CONNECTING: 242 _onConnectingHandler = handler; 243 break; 244 case _STATUS.CONNECTED: 245 _onConnectHandler = handler; 246 break; 247 case _STATUS.DISCONNECTED: 248 _onDisconnectHandler = handler; 249 break; 250 case _STATUS.DISCONNECTING: 251 _onDisconnectingHandler = handler; 252 break; 253 case _STATUS.RECONNECTING: 254 _onReconnectingHandler = handler; 255 break; 256 case _STATUS.UNLOADING: 257 _onUnloadingHandler = handler; 258 break; 259 } 260 261 } else { 262 throw new Error("Callback is not a function"); 263 } 264 }; 265 266 return { 267 /** 268 * @private 269 * Indicates whether the ClientServices instance is a Master. 270 * @returns {Boolean} 271 * True if this instance of ClientServices is a Master, false otherwise. 272 */ 273 isMaster: function () { 274 return _isMaster; 275 }, 276 277 /** 278 * @private 279 * Get the resource ID. An ID is only available if the BOSH connection has 280 * been able to connect successfully. 281 * @returns {String} 282 * The resource ID string. Null if the BOSH connection was never 283 * successfully created and/or the resource ID has not been associated. 284 */ 285 getResourceID: function () { 286 if (_connInfo !== undefined) { 287 return _connInfo.resourceID; 288 } 289 return null; 290 }, 291 292 /** 293 * @private 294 * Add a callback to be invoked when the BOSH connection is attempting 295 * to connect. If the connection is already trying to connect, the 296 * callback will be invoked immediately. 297 * @param {Function} handler 298 * An empty param function to be invoked on connecting. Only one 299 * handler can be registered at a time. Handlers already registered 300 * will be overwritten. 301 */ 302 registerOnConnectingHandler: function (handler) { 303 _registerHandler(_STATUS.CONNECTING, handler); 304 }, 305 306 /** 307 * @private 308 * Removes the on connecting callback that was registered. 309 */ 310 unregisterOnConnectingHandler: function () { 311 _onConnectingHandler = undefined; 312 }, 313 314 /** 315 * @private 316 * Add a callback to be invoked when the BOSH connection has been 317 * established. If the connection has already been established, the 318 * callback will be invoked immediately. 319 * @param {Function} handler 320 * An empty param function to be invoked on connect. Only one handler 321 * can be registered at a time. Handlers already registered will be 322 * overwritten. 323 */ 324 registerOnConnectHandler: function (handler) { 325 _registerHandler(_STATUS.CONNECTED, handler); 326 }, 327 328 /** 329 * @private 330 * Removes the on connect callback that was registered. 331 */ 332 unregisterOnConnectHandler: function () { 333 _onConnectHandler = undefined; 334 }, 335 336 /** 337 * @private 338 * Add a callback to be invoked when the BOSH connection goes down. If 339 * the connection is already down, invoke the callback immediately. 340 * @param {Function} handler 341 * An empty param function to be invoked on disconnected. Only one 342 * handler can be registered at a time. Handlers already registered 343 * will be overwritten. 344 */ 345 registerOnDisconnectHandler: function (handler) { 346 _registerHandler(_STATUS.DISCONNECTED, handler); 347 }, 348 349 /** 350 * @private 351 * Removes the on disconnect callback that was registered. 352 */ 353 unregisterOnDisconnectHandler: function () { 354 _onDisconnectHandler = undefined; 355 }, 356 357 /** 358 * @private 359 * Add a callback to be invoked when the BOSH is currently disconnecting. If 360 * the connection is already disconnecting, invoke the callback immediately. 361 * @param {Function} handler 362 * An empty param function to be invoked on disconnected. Only one 363 * handler can be registered at a time. Handlers already registered 364 * will be overwritten. 365 */ 366 registerOnDisconnectingHandler: function (handler) { 367 _registerHandler(_STATUS.DISCONNECTING, handler); 368 }, 369 370 /** 371 * @private 372 * Removes the on disconnecting callback that was registered. 373 */ 374 unregisterOnDisconnectingHandler: function () { 375 _onDisconnectingHandler = undefined; 376 }, 377 378 /** 379 * @private 380 * Add a callback to be invoked when the BOSH connection is attempting 381 * to connect. If the connection is already trying to connect, the 382 * callback will be invoked immediately. 383 * @param {Function} handler 384 * An empty param function to be invoked on connecting. Only one 385 * handler can be registered at a time. Handlers already registered 386 * will be overwritten. 387 */ 388 registerOnReconnectingHandler: function (handler) { 389 _registerHandler(_STATUS.RECONNECTING, handler); 390 }, 391 392 /** 393 * @private 394 * Removes the on reconnecting callback that was registered. 395 */ 396 unregisterOnReconnectingHandler: function () { 397 _onReconnectingHandler = undefined; 398 }, 399 400 /** 401 * @private 402 * Add a callback to be invoked when the BOSH connection is unloading 403 * 404 * @param {Function} handler 405 * An empty param function to be invoked on connecting. Only one 406 * handler can be registered at a time. Handlers already registered 407 * will be overwritten. 408 */ 409 registerOnUnloadingHandler: function (handler) { 410 _registerHandler(_STATUS.UNLOADING, handler); 411 }, 412 413 /** 414 * @private 415 * Removes the on unloading callback that was registered. 416 */ 417 unregisterOnUnloadingHandler: function () { 418 _onUnloadingHandler = undefined; 419 }, 420 421 /** 422 * @private 423 * Proxy method for gadgets.io.makeRequest. The will be identical to gadgets.io.makeRequest 424 * ClientServices will mixin the BASIC Auth string, locale, and host, since the 425 * configuration is encapsulated in here anyways. 426 * This removes the dependency 427 * @param {String} url 428 * The relative url to make the request to (the host from the passed in config will be 429 * appended). It is expected that any encoding to the URL is already done. 430 * @param {Function} handler 431 * Callback handler for makeRequest to invoke when the response returns. 432 * Completely passed through to gadgets.io.makeRequest 433 * @param {Object} params 434 * The params object that gadgets.io.makeRequest expects. Authorization and locale 435 * headers are mixed in. 436 */ 437 makeRequest: function (url, handler, params) { 438 // ClientServices needs to be initialized with a config for restHost, auth, and locale 439 _isInited(); 440 441 // Allow mixin of auth and locale headers 442 params = params || {}; 443 params[gadgets.io.RequestParameters.HEADERS] = params[gadgets.io.RequestParameters.HEADERS] || {}; 444 445 // Add Basic auth to request header 446 params[gadgets.io.RequestParameters.HEADERS].Authorization = "Basic " + _config.authorization; 447 //Locale 448 params[gadgets.io.RequestParameters.HEADERS].locale = _config.locale; 449 450 gadgets.io.makeRequest(encodeURI("http://" + _config.restHost) + url, handler, params); 451 }, 452 453 /** 454 * @private 455 * Utility function to make a subscription to a particular topic. Only one 456 * callback function is registered to a particular topic at any time. 457 * @param {String} topic 458 * The full topic name. The topic name should follow the OpenAjax 459 * convention using dot notation (ex: finesse.api.User.1000). 460 * @param {Function} callback 461 * The function that should be invoked with the data when an event 462 * is delivered to the specific topic. 463 * @returns {Boolean} 464 * True if the subscription was made successfully and the callback was 465 * been registered. False if the subscription already exist, the 466 * callback was not overwritten. 467 */ 468 subscribe: function (topic, callback) { 469 _isInited(); 470 471 //Ensure that the same subscription isn't made twice. 472 if (!_subscriptionID[topic]) { 473 //Store the subscription ID using the topic name as the key. 474 _subscriptionID[topic] = _hub.subscribe(topic, 475 //Invoke the callback just with the data object. 476 function (topic, data) { 477 callback(data); 478 }); 479 return true; 480 } 481 return false; 482 }, 483 484 /** 485 * @private 486 * Unsubscribe from a particular topic. 487 * @param {String} topic 488 * The full topic name. 489 */ 490 unsubscribe: function (topic) { 491 _isInited(); 492 493 //Unsubscribe from the topic using the subscription ID recorded when 494 //the subscription was made, then delete the ID from data structure. 495 _hub.unsubscribe(_subscriptionID[topic]); 496 delete _subscriptionID[topic]; 497 }, 498 499 /** 500 * @private 501 * Make a request to the request channel to have the Master subscribe 502 * to a node. 503 * @param {String} node 504 * The node to subscribe to. 505 */ 506 subscribeNode: function (node, handler) { 507 if (handler && typeof handler !== "function") { 508 throw new Error("ClientServices.subscribeNode: handler is not a function"); 509 } 510 511 // Construct the request to send to MasterPublisher through the OpenAjax Hub 512 var data = { 513 type: "SubscribeNodeReq", 514 data: {node: node}, 515 invokeID: _util.generateUUID() 516 }, 517 responseTopic = _topics.RESPONSES + "." + data.invokeID, 518 _this = this; 519 520 // We need to first subscribe to the response channel 521 this.subscribe(responseTopic, function (rsp) { 522 // Since this channel is only used for this singular request, 523 // we are not interested anymore. 524 // This is also critical to not leaking memory by having OpenAjax 525 // store a bunch of orphaned callback handlers that enclose on 526 // our entire ClientServices singleton 527 _this.unsubscribe(responseTopic); 528 if (handler) { 529 handler(data.invokeID, rsp); 530 } 531 }); 532 // Then publish the request on the request channel 533 _hub.publish(_topics.REQUESTS, data); 534 }, 535 536 /** 537 * @private 538 * Make a request to the request channel to have the Master unsubscribe 539 * from a node. 540 * @param {String} node 541 * The node to unsubscribe from. 542 */ 543 unsubscribeNode: function (node, subid, handler) { 544 if (handler && typeof handler !== "function") { 545 throw new Error("ClientServices.unsubscribeNode: handler is not a function"); 546 } 547 548 // Construct the request to send to MasterPublisher through the OpenAjax Hub 549 var data = { 550 type: "UnsubscribeNodeReq", 551 data: { 552 node: node, 553 subid: subid 554 }, 555 invokeID: _util.generateUUID() 556 }, 557 responseTopic = _topics.RESPONSES + "." + data.invokeID, 558 _this = this; 559 560 // We need to first subscribe to the response channel 561 this.subscribe(responseTopic, function (rsp) { 562 // Since this channel is only used for this singular request, 563 // we are not interested anymore. 564 // This is also critical to not leaking memory by having OpenAjax 565 // store a bunch of orphaned callback handlers that enclose on 566 // our entire ClientServices singleton 567 _this.unsubscribe(responseTopic); 568 if (handler) { 569 handler(data.invokeID, rsp); 570 } 571 }); 572 // Then publish the request on the request channel 573 _hub.publish(_topics.REQUESTS, data); 574 }, 575 576 /** 577 * @private 578 * Make a request to the request channel to have the Master connect to the XMPP server via BOSH 579 */ 580 makeConnectionReq : function () { 581 var data = { 582 type: "ConnectionReq", 583 data: { 584 id: _config.id, 585 password: _config.password, 586 xmppDomain: _config.xmppDomain 587 }, 588 invokeID: (new Date()).getTime() 589 }; 590 _hub.publish(_topics.REQUESTS, data); 591 }, 592 593 /** 594 * Initiates the Client Services with the specified config parameters. 595 * Enabling the Client Services as Master will trigger the establishment 596 * of a BOSH event connection. 597 * @param {Object} config 598 * Configuration object containing properties used for making REST requests:<ul> 599 * <li><b>host:</b> The Finesse server IP/host as reachable from the browser 600 * <li><b>restHost:</b> The Finesse API IP/host as reachable from the gadget container 601 * <li><b>id:</b> The ID of the user. This is an optional param as long as the 602 * appropriate authorization string is provided, otherwise it is 603 * required.</li> 604 * <li><b>password:</b> The password belonging to the user. This is an optional param as 605 * long as the appropriate authorization string is provided, 606 * otherwise it is required.</li> 607 * <li><b>authorization:</b> The base64 encoded "id:password" authentication string. This 608 * param is provided to allow the ability to hide the password 609 * param. If provided, the id and the password extracted from this 610 * string will be used over the config.id and config.password.</li> 611 * </ul> 612 * @throws {Error} If required constructor parameter is missing. 613 */ 614 init: function (config) { 615 if (!_inited) { 616 //Validate the properties within the config object if one is provided. 617 if (!(typeof config === "object" && 618 typeof config.host === "string" && config.host.length > 0 && config.restHost && 619 (typeof config.authorization === "string" || 620 (typeof config.id === "string" && 621 typeof config.password === "string")))) { 622 throw new Error("Config object contains invalid properties."); 623 } 624 625 // Initialize configuration 626 _config = config; 627 628 // Set shortcuts 629 _util = finesse.utilities.Utilities; 630 _hub = gadgets.Hub; 631 _io = gadgets.io; 632 _topics = finesse.clientservices.Topics; 633 634 //If the authorization string is provided, then use that to 635 //extract the ID and the password. Otherwise use the ID and 636 //password from the respective ID and password params. 637 if (_config.authorization) { 638 var creds = _util.getCredentials(_config.authorization); 639 _config.id = creds.id; 640 _config.password = creds.password; 641 } 642 else { 643 _config.authorization = _util.b64Encode( 644 _config.id + ":" + _config.password); 645 } 646 647 _inited = true; 648 649 if (_hub) { 650 //Subscribe to receive connection information. Since it is possible that 651 //the client comes up after the Master comes up, the client will need 652 //to make a request to have the Master send the latest connection info. 653 //It would be possible that all clients get connection info again. 654 this.subscribe(_topics.EVENTS_CONNECTION_INFO, _connInfoHandler); 655 _makeConnectionInfoReq(); 656 } 657 } 658 659 //Return the CS object for object chaining. 660 return this; 661 }, 662 663 /** 664 * @private 665 * Initializes the BOSH component of this ClientServices instance. This establishes 666 * the BOSH connection and will trigger the registered handlers as the connection 667 * status changes respectively:<ul> 668 * <li>registerOnConnectingHandler</li> 669 * <li>registerOnConnectHandler</li> 670 * <li>registerOnDisconnectHandler</li> 671 * <li>registerOnDisconnectingHandler</li> 672 * <li>registerOnReconnectingHandler</li> 673 * <li>registerOnUnloadingHandler</li> 674 * <ul> 675 * 676 * @param {Object} config 677 * An object containing the following (optional) handlers for the request:<ul> 678 * <li><b>xmppDomain:</b> {String} The domain of the XMPP server. Available from the SystemInfo object. 679 * This is used to construct the JID: user@domain.com</li> 680 * <li><b>pubsubDomain:</b> {String} The pub sub domain where the pub sub service is running. 681 * Available from the SystemInfo object. 682 * This is used for creating or removing subscriptions.</li> 683 * </ul> 684 */ 685 initBosh: function (config) { 686 //Validate the properties within the config object if one is provided. 687 if (!(typeof config === "object" && typeof config.xmppDomain === "string" && typeof config.pubsubDomain === "string")) { 688 throw new Error("Config object contains invalid properties."); 689 } 690 691 // Mixin the required information for establishing the BOSH connection 692 _config.xmppDomain = config.xmppDomain; 693 _config.pubsubDomain = config.pubsubDomain; 694 695 //Initiate Master launch sequence 696 _becomeMaster(); 697 }, 698 699 //BEGIN TEST CODE// 700 /** 701 * Test code added to expose private functions that are used by unit test 702 * framework. This section of code is removed during the build process 703 * before packaging production code. The [begin|end]TestSection are used 704 * by the build to identify the section to strip. 705 * @ignore 706 */ 707 beginTestSection : 0, 708 709 /** 710 * @ignore 711 */ 712 getTestObject: function () { 713 //Load mock dependencies. 714 var _mock = new MockControl(); 715 _hub = _mock.createMock(gadgets.Hub); 716 _io = _mock.createMock(gadgets.io); 717 718 return { 719 //Expose mock dependencies 720 mock: _mock, 721 hub: _hub, 722 io: _io, 723 724 //Expose internal private functions 725 subscriptionID: _subscriptionID, 726 connInfoHandler: _connInfoHandler, 727 728 reset: function () { 729 _inited = false; 730 _onConnectingHandler = undefined; 731 _onConnectHandler = undefined; 732 _onDisconnectingHandler = undefined; 733 _onDisconnectHandler = undefined; 734 _onReconnectingHandler = undefined; 735 _connInfo = undefined; 736 }, 737 setUtil: function () { 738 _util = finesse.utilities.Utilities; 739 }, 740 setConfig: function (config) { 741 _config = config; 742 } 743 }; 744 }, 745 746 /** 747 * @ignore 748 */ 749 endTestSection: 0 750 //END TEST CODE// 751 }; 752 }()); 753