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: "websocket",
 15       fallbackTransport: "long-polling",
 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));