1 (function($, rf, jsf) { 2 3 4 /** 5 * Global object accessing general state of Push functionality. 6 * 7 * Push internally uses Atmosphere to handle connection. 8 * 9 * @module RichFaces.push 10 */ 11 rf.push = { 12 13 options: { 14 transport: "long-polling", 15 fallbackTransport: undefined, 16 logLevel: "info" 17 }, 18 19 /** 20 * topics that we subscribed to in current atmosphere connection 21 */ 22 _subscribedTopics: {}, 23 24 /** 25 * topics to be added to subscription during next invocation updateConnection 26 */ 27 _addedTopics: {}, 28 29 /** 30 * topics to be removed from subscription during next invocation updateConnection 31 */ 32 _removedTopics: {}, 33 34 /** 35 * Object that holds mapping of addresses to number of subscribed widgets 36 * 37 * { address: counter } 38 * 39 * If counter reaches 0, it is safe to unsubscribe given address. 40 */ 41 _handlersCounter: {}, 42 43 /** 44 * current Push session identifier 45 */ 46 _pushSessionId: null, 47 /** 48 * sequence number which identifies which messages were already processed 49 */ 50 _lastMessageNumber: -1, 51 52 /** 53 * the URL that handles Push subscriptions 54 */ 55 _pushResourceUrl: null, 56 /** 57 * the URL that handles Atmosphere requests 58 */ 59 _pushHandlerUrl: null, 60 61 /** 62 * Checks whether there are requests for adding or removing topics to the list of subscribed topics. 63 * 64 * If yes, it reconnects Atmosphere connection. 65 * 66 * @method updateConnection 67 */ 68 updateConnection: function() { 69 if ($.isEmptyObject(this._handlersCounter)) { 70 this._disconnect(); 71 } else if (!$.isEmptyObject(this._addedTopics) || !$.isEmptyObject(this._removedTopics)) { 72 this._disconnect(); 73 this._connect(); 74 } 75 76 this._addedTopics = {}; 77 this._removedTopics = {}; 78 }, 79 80 /** 81 * Increases number of subscriptions to given topic 82 * 83 * @method increaseSubscriptionCounters 84 * @param topic 85 */ 86 increaseSubscriptionCounters: function(topic) { 87 if (isNaN(this._handlersCounter[topic]++)) { 88 this._handlersCounter[topic] = 1; 89 this._addedTopics[topic] = true; 90 } 91 }, 92 93 /** 94 * Decreases number of subscriptions to given topic 95 * 96 * @method decreaseSubscriptionCounters 97 * @param topic 98 */ 99 decreaseSubscriptionCounters: function(topic) { 100 if (--this._handlersCounter[topic] == 0) { 101 delete this._handlersCounter[topic]; 102 this._removedTopics[topic] = true; 103 this._subscribedTopics[topic] = false; 104 } 105 }, 106 107 /** 108 * Setups the URL that handles Push subscriptions 109 * 110 * @method setPushResourceUrl 111 * @param url 112 */ 113 setPushResourceUrl: function(url) { 114 this._pushResourceUrl = qualifyUrl(url); 115 }, 116 117 /** 118 * Setups the URL that handles Atmosphere requests 119 * 120 * @method setPushHandlerUrl 121 * @param url 122 */ 123 setPushHandlerUrl: function(url) { 124 this._pushHandlerUrl = qualifyUrl(url); 125 }, 126 127 /** 128 * Handles messages transported using Atmosphere 129 */ 130 _messageCallback: function(response) { 131 var suspendMessageEndMarker = /^(<!--[^>]+-->\s*)+/; 132 var messageTokenExpr = /<msg topic="([^"]+)" number="([^"]+)">([^<]*)<\/msg>/g; 133 134 var dataString = response.responseBody.replace(suspendMessageEndMarker, ""); 135 if (dataString) { 136 var messageToken; 137 while (messageToken = messageTokenExpr.exec(dataString)) { 138 if (!messageToken[1]) { 139 continue; 140 } 141 142 var message = { 143 topic: messageToken[1], 144 number: parseInt(messageToken[2]), 145 data: $.parseJSON(messageToken[3]) 146 }; 147 148 if (message.number <= this._lastMessageNumber) { 149 continue; 150 } 151 152 var event = new jQuery.Event('push.push.RICH.' + message.topic, { rf: { data: message.data } }); 153 154 (function(event) { 155 $(function() { 156 $(document).trigger(event); 157 }); 158 })(event); 159 160 this._lastMessageNumber = message.number; 161 } 162 } 163 }, 164 165 /** 166 * Handles errors during Atmosphere initialization and transport 167 */ 168 _errorCallback: function(response) { 169 for (var address in newlySubcribed) { 170 this._subscribedTopics[address] = true; 171 $(document).trigger('error.push.RICH.' + address, response); 172 } 173 }, 174 175 /** 176 * Initializes Atmosphere connection 177 */ 178 _connect: function() { 179 var newlySubcribed = {}; 180 181 var topics = []; 182 for (var address in this._handlersCounter) { 183 topics.push(address); 184 if (!this._subscribedTopics[address]) { 185 newlySubcribed[address] = true; 186 } 187 } 188 189 var data = { 190 'pushTopic': topics 191 }; 192 193 if (this._pushSessionId) { 194 data['forgetPushSessionId'] = this._pushSessionId; 195 } 196 197 //TODO handle request errors 198 $.ajax({ 199 data: data, 200 dataType: 'text', 201 traditional: true, 202 type: 'POST', 203 url: this._pushResourceUrl, 204 success: $.proxy(function(response) { 205 var data = $.parseJSON(response); 206 207 for (var address in data.failures) { 208 $(document).trigger('error.push.RICH.' + address); 209 } 210 211 if (data.sessionId) { 212 this._pushSessionId = data.sessionId; 213 214 var url = this._pushHandlerUrl || this._pushResourceUrl; 215 url += "?__richfacesPushAsync=1&pushSessionId=" 216 url += this._pushSessionId; 217 218 var messageCallback = $.proxy(this._messageCallback, this); 219 var errorCallback = $.proxy(this._errorCallback, this); 220 221 $.atmosphere.subscribe(url, messageCallback, { 222 transport: this.options.transport, 223 fallbackTransport: this.options.fallbackTransport, 224 logLevel: this.options.logLevel, 225 onError: errorCallback 226 }); 227 228 // fire subscribed events 229 for (var address in newlySubcribed) { 230 this._subscribedTopics[address] = true; 231 $(document).trigger('subscribed.push.RICH.' + address); 232 } 233 } 234 }, this) 235 }); 236 }, 237 238 /** 239 * Ends Atmosphere connection 240 */ 241 _disconnect: function() { 242 $.atmosphere.unsubscribe(); 243 } 244 }; 245 246 /** 247 * jQuery plugin which mades easy to bind the Push to the DOM element 248 * and manage plugins lifecycle and event handling 249 * 250 * @function $.fn.richpush 251 */ 252 253 $.fn.richpush = function( options ) { 254 var widget = $.extend({}, $.fn.richpush); 255 256 // for all selected elements 257 return this.each(function() { 258 widget.element = this; 259 widget.options = $.extend({}, widget.options, options); 260 widget.eventNamespace = '.push.RICH.' + widget.element.id; 261 262 // call constructor 263 widget._create(); 264 265 // listen for global DOM destruction event 266 $(document).on('beforeDomClean' + widget.eventNamespace, function(event) { 267 // is the push component under destructed DOM element (event target)? 268 if (event.target && (event.target === widget.element || $.contains(event.target, widget.element))) { 269 widget._destroy(); 270 } 271 }); 272 }); 273 }; 274 275 $.extend($.fn.richpush, { 276 277 options: { 278 279 /** 280 * Specifies which address (topic) will Push listen on 281 * 282 * @property address 283 * @required 284 */ 285 address: null, 286 287 /** 288 * Fired when Push is subscribed to the specified address 289 * 290 * @event subscribed 291 */ 292 subscribed: null, 293 294 /** 295 * Triggered when Push receives data 296 * 297 * @event push 298 */ 299 push: null, 300 301 /** 302 * Triggered when error is observed during Push initialization or communication 303 * 304 * @event error 305 */ 306 error: null 307 }, 308 309 _create: function() { 310 var widget = this; 311 312 this.address = this.options.address; 313 314 this.handlers = { 315 subscribed: null, 316 push: null, 317 error: null 318 }; 319 320 $.each(this.handlers, function(eventName) { 321 if (widget.options[eventName]) { 322 323 var handler = function(event, data) { 324 if (data) { 325 $.extend(event, { 326 rf: { 327 data: data 328 } 329 }); 330 } 331 332 widget.options[eventName].call(widget.element, event); 333 }; 334 335 widget.handlers[eventName] = handler; 336 $(document).on(eventName + widget.eventNamespace + '.' + widget.address, handler); 337 } 338 }); 339 340 rf.push.increaseSubscriptionCounters(this.address); 341 }, 342 343 _destroy: function() { 344 rf.push.decreaseSubscriptionCounters(this.address); 345 $(document).off(this.eventNamespace); 346 } 347 348 }); 349 350 /* 351 * INITIALIZE PUSH 352 * 353 * when document is ready and when JSF errors happens 354 */ 355 356 $(document).ready(function() { 357 rf.push.updateConnection(); 358 }); 359 360 jsf.ajax.addOnEvent(jsfErrorHandler); 361 jsf.ajax.addOnError(jsfErrorHandler); 362 363 364 /* PRIVATE FUNCTIONS */ 365 366 function jsfErrorHandler(event) { 367 if (event.type == 'event') { 368 if (event.status != 'success') { 369 return; 370 } 371 } else if (event.type != 'error') { 372 return; 373 } 374 375 rf.push.updateConnection(); 376 } 377 378 function qualifyUrl(url) { 379 var result = url; 380 if (url.charAt(0) == '/') { 381 result = location.protocol + '//' + location.host + url; 382 } 383 return result; 384 } 385 386 }(RichFaces.jQuery, RichFaces, jsf));