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