      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));