1 /*
  2  * JBoss, Home of Professional Open Source
  3  * Copyright 2013, Red Hat, Inc. and individual contributors
  4  * by the @authors tag. See the copyright.txt in the distribution for a
  5  * full listing of individual contributors.
  6  *
  7  * This is free software; you can redistribute it and/or modify it
  8  * under the terms of the GNU Lesser General Public License as
  9  * published by the Free Software Foundation; either version 2.1 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * This software is distributed in the hope that it will be useful,
 13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 15  * Lesser General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Lesser General Public
 18  * License along with this software; if not, write to the Free
 19  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 20  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 21  */
 22 
 23 /**
 24  * @author Pavel Yaschenko
 25  */
 26 
 27 (function($, rf, jsf) {
 28 
 29     /**
 30      * RichFaces Ajax container
 31      * @class
 32      * @memberOf RichFaces
 33      * @static
 34      * @name ajaxContainer
 35      * */
 36     rf.ajaxContainer = rf.ajaxContainer || {};
 37 
 38     if (rf.ajaxContainer.jsfRequest) {
 39         return;
 40     }
 41 
 42     /**
 43      * JSF 2.0 original method that sends an asynchronous ajax request to the server
 44      * see jsf.ajax.request method for parameter's description
 45      * @function
 46      * @name RichFaces.ajaxContainer.jsfRequest
 47      *
 48      * */
 49     rf.ajaxContainer.jsfRequest = jsf.ajax.request;
 50 
 51     /**
 52      * RichFaces wrapper function of JSF 2.0 original method jsf.ajax.request
 53      * @function
 54      * @name jsf.ajax.request
 55      *
 56      * @param {string|DOMElement} source - The DOM element or an id that triggered this ajax request
 57      * @param {object} [event] - The DOM event that triggered this ajax request
 58      * @param {object} [options] - The set name/value pairs that can be sent as request parameters to control client and/or server side request processing
 59      * */
 60     jsf.ajax.request = function(source, event, options) {
 61         rf.queue.push(source, event, options);
 62     };
 63 
 64     rf.ajaxContainer.jsfResponse = jsf.ajax.response;
 65 
 66     rf.ajaxContainer.isIgnoreResponse = function() {
 67         return rf.queue.isIgnoreResponse();
 68     };
 69 
 70 
 71     jsf.ajax.response = function(request, context) {
 72         rf.queue.response(request, context);
 73     };
 74 
 75     var QUEUE_MODE_PULL = 'pull';
 76     var QUEUE_MODE_PUSH = 'push';
 77     var QUEUE_MODE = QUEUE_MODE_PULL;
 78     var DEFAULT_QUEUE_ID = "org.richfaces.queue.global";
 79 
 80     /**
 81      * RichFaces Queue API container
 82      * @class
 83      * @memberOf RichFaces
 84      * @static
 85      * @name queue
 86      * */
 87     rf.queue = (function() {
 88 
 89         var defaultQueueOptions = {};
 90         //defaultQueueOptions[DEFAULT_QUEUE_ID] = {requestDelay:0, ignoreDupResponse:false, timeout:0};
 91         var eventHandlers = {};
 92 
 93         var QueueEntry = function(queue, source, event, options) {
 94             this.queue = queue;
 95             this.source = source;
 96             this.options = $.extend({}, options || {});
 97             this.queueOptions = {}
 98             var id;
 99 
100             // find default options for QueueEntry
101             if (this.options.queueId) {
102                 if (defaultQueueOptions[this.options.queueId]) {
103                     id = this.options.queueId;
104                 }
105                 delete this.options.queueId;
106             } else {
107                 var element = rf.getDomElement(source);
108                 var form;
109                 if (element) {
110                     element = $(element).closest("form");
111                     if (element.length > 0) {
112                         form = element.get(0);
113                     }
114                 }
115                 if (form && form.id && defaultQueueOptions[form.id]) {
116                     id = form.id;
117                 } else {
118                     id = DEFAULT_QUEUE_ID;
119                 }
120             }
121             if (id) {
122                 this.queueOptions = defaultQueueOptions[id] || {};
123                 if (this.queueOptions.queueId) {
124                     this.queueOptions = $.extend({}, (defaultQueueOptions[this.queueOptions.queueId] || {}), this.queueOptions);
125                 } else {
126                     // TODO: clean duplicated code
127                     var element = rf.getDomElement(source);
128                     var form;
129                     if (element) {
130                         element = $(element).closest("form");
131                         if (element.length > 0) {
132                             form = element.get(0);
133                         }
134                     }
135                     if (form && form.id && defaultQueueOptions[form.id]) {
136                         id = form.id;
137                     } else {
138                         id = DEFAULT_QUEUE_ID;
139                     }
140                     if (id) {
141                         this.queueOptions = $.extend({}, (defaultQueueOptions[id] || {}), this.queueOptions);
142                     }
143                 }
144             }
145 
146             if (typeof this.queueOptions.requestGroupingId == "undefined") {
147                 this.queueOptions.requestGroupingId = typeof this.source == "string" ? this.source : this.source.id;
148             }
149 
150             // Remove the layerX and layerY events (generated in WebKit browsers)
151             if (event && event instanceof Object) {
152                 if ('layerX' in event) delete event.layerX;
153                 if ('layerY' in event) delete event.layerY;
154             }
155 
156             // copy of event should be created otherwise IE will fail
157             this.event = $.extend({}, event);
158 
159             //requestGroupingId is mutable, thus we need special field for it
160             this.requestGroupingId = this.queueOptions.requestGroupingId;
161             this.eventsCount = 1;
162         };
163 
164         $.extend(QueueEntry.prototype, {
165                 // now unused functions: ondrop, clearEntry
166                 isIgnoreDupResponses: function() {
167                     return this.queueOptions.ignoreDupResponses;
168                 },
169 
170                 getRequestGroupId: function() {
171                     return this.requestGroupingId;
172                 },
173 
174                 setRequestGroupId: function(id) {
175                     this.requestGroupingId = id;
176                 },
177 
178                 resetRequestGroupId: function() {
179                     this.requestGroupingId = undefined;
180                 },
181 
182                 setReadyToSubmit: function(isReady) {
183                     this.readyToSubmit = isReady;
184                 },
185 
186                 getReadyToSubmit: function() {
187                     return this.readyToSubmit;
188                 },
189 
190                 ondrop: function() {
191                     var callback = this.queueOptions.onqueuerequestdrop;
192                     if (callback) {
193                         callback.call(this.queue, this.source, this.options, this.event);
194                     }
195                 },
196 
197                 onRequestDelayPassed: function() {
198                     this.readyToSubmit = true;
199                     submitFirstEntry.call(this.queue);
200                 },
201 
202                 startTimer: function() {
203                     var delay = this.queueOptions.requestDelay;
204                     if (typeof delay != "number") {
205                         delay = this.queueOptions.requestDelay || 0;
206                     }
207 
208                     rf.log.debug("Queue will wait " + (delay || 0) + "ms before submit");
209 
210                     if (delay) {
211                         var _this = this;
212                         this.timer = window.setTimeout(function() {
213                             try {
214                                 _this.onRequestDelayPassed();
215                             } finally {
216                                 _this.timer = undefined;
217                                 _this = undefined;
218                             }
219                         }, delay);
220                     } else {
221                         this.onRequestDelayPassed();
222                     }
223                 },
224 
225                 stopTimer: function() {
226                     if (this.timer) {
227                         window.clearTimeout(this.timer);
228                         this.timer = undefined;
229                     }
230                 },
231 
232                 clearEntry: function() { //???
233                     this.stopTimer();
234                     if (this.request) {
235                         this.request.shouldNotifyQueue = false;
236                         this.request = undefined;
237                     }
238                 },
239 
240                 getEventsCount: function() {
241                     return this.eventsCount;
242                 },
243 
244                 setEventsCount: function(newCount) {
245                     this.eventsCount = newCount;
246                 }
247             });
248 
249         // TODO: add this two variables to richfaces and report bug to jsf about constants
250         var JSF_EVENT_TYPE = 'event';
251         var JSF_EVENT_SUCCESS = 'success';
252         var JSF_EVENT_COMPLETE = 'complete';
253 
254         var items = [];
255         var lastRequestedEntry;
256 
257         //TODO: instance of this function will be created for each queue
258         var onError = function (data) {
259             var message = "richfaces.queue: ajax submit error";
260             if (data) {
261                 var description = data.message || data.description;
262                 if (description) {
263                     message += ": " + description;
264                 }
265             }
266             rf.log.warn(message);
267 
268             lastRequestedEntry = null;
269             //TODO: what if somebody is going to clear queue on error?
270             submitFirstEntry();
271         };
272 
273         var removeStaleEntriesFromQueue = function () {
274             var entry;
275             var foundValidEntry = false;
276             while (items.length > 0 && !foundValidEntry) {
277                 entry = items[0];
278                 var element = rf.getDomElement(entry.source);
279                 if (element == null || $(element).closest("form").length == 0) {
280                     var removedEntry = items.shift();
281                     removedEntry.stopTimer();
282                     rf.log.debug("richfaces.queue: removing stale entry from the queue (source element: " + element + ")");
283                 } else {
284                     foundValidEntry = true;
285                 }
286             }
287         }
288 
289         var onComplete = function (data) {
290             if (data.type == JSF_EVENT_TYPE && data.status == JSF_EVENT_SUCCESS) { // or JSF_EVENT_COMPLETE will be rather
291                 rf.log.debug("richfaces.queue: ajax submit successfull");
292                 lastRequestedEntry = null;
293                 removeStaleEntriesFromQueue();
294                 submitFirstEntry();
295             }
296         };
297 
298         jsf.ajax.addOnEvent(onComplete);
299         jsf.ajax.addOnError(onError);
300 
301         var submitFirstEntry = function() {
302             if (QUEUE_MODE == QUEUE_MODE_PULL && lastRequestedEntry) {
303                 rf.log.debug("richfaces.queue: Waiting for previous submit results");
304                 return;
305             }
306             if (isEmpty()) {
307                 rf.log.debug("richfaces.queue: Nothing to submit");
308                 return;
309             }
310             var entry;
311             if (items[0].getReadyToSubmit()) {
312             	try {
313                     entry = lastRequestedEntry = items.shift();
314                     rf.log.debug("richfaces.queue: will submit request NOW");
315                     var o = lastRequestedEntry.options;
316                     o["AJAX:EVENTS_COUNT"] = lastRequestedEntry.eventsCount;
317                     rf.ajaxContainer.jsfRequest(lastRequestedEntry.source, lastRequestedEntry.event, o);
318 
319                     // call event handlers
320                     if (o.queueonsubmit) {
321                         o.queueonsubmit.call(entry);
322                     }
323                     callEventHandler("onrequestdequeue", entry);
324             	} catch (error) {
325                     onError(error);
326             	}
327             }
328         };
329 
330         var isEmpty = function() {
331             return (getSize() == 0)
332         };
333         var getSize = function() {
334             return items.length;
335         };
336 
337         var getLastEntry = function () {
338             var lastIdx = items.length - 1;
339             return items[lastIdx];
340         };
341 
342         var updateLastEntry = function (entry) {
343             var lastIdx = items.length - 1;
344             items[lastIdx] = entry;
345         };
346 
347         var callEventHandler = function (handlerName, entry) {
348             var handler = entry.queueOptions[handlerName];
349             if (handler) {
350                 if (typeof(handler) == "string") {
351                     new Function(handler).call(null, entry);
352                 } else {
353                     handler.call(null, entry);
354                 }
355             }
356             var opts, handler2;
357             if (entry.queueOptions.queueId &&
358                 (opts = defaultQueueOptions[entry.queueOptions.queueId]) &&
359                 (handler2 = opts[handlerName])
360                 && handler2 != handler) {
361                 // the same about context
362                 handler2.call(null, entry);
363             }
364         }
365 
366         var pushEntry = function (entry) {
367             items.push(entry);
368             rf.log.debug("New request added to queue. Queue requestGroupingId changed to " + entry.getRequestGroupId());
369             // call event handlers
370             callEventHandler("onrequestqueue", entry);
371         }
372 
373         return {
374             /**
375              * @constant
376              * @name RichFaces.queue.DEFAULT_QUEUE_ID
377              * @type string
378              * */
379             DEFAULT_QUEUE_ID: DEFAULT_QUEUE_ID,
380 
381             /**
382              * Get current queue size
383              * @function
384              * @name RichFaces.queue.getSize
385              *
386              * @return {number} size of items in the queue
387              * */
388             getSize: getSize,
389 
390             /**
391              * Check if queue is empty
392              * @function
393              * @name RichFaces.queue.isEmpty
394              *
395              * @return {boolean} returns true if queue is empty
396              * */
397             isEmpty: isEmpty,
398 
399             /**
400              * Extract and submit first QueueEntry in the queue if QueueEntry is ready to submit
401              * @function
402              * @name RichFaces.queue.submitFirst
403              * */
404             submitFirst: function () {
405                 if (!isEmpty()) {
406                     var entry = items[0];
407                     entry.stopTimer();
408                     entry.setReadyToSubmit(true);
409                     submitFirstEntry();
410                 }
411             },
412 
413             /**
414              * Create and push QueueEntry to the queue for ajax requests
415              * @function
416              * @name RichFaces.queue.push
417              *
418              * @param {string|DOMElement} source - The DOM element or an id that triggered this ajax request
419              * @param {object} [event] - The DOM event that triggered this ajax request
420              * @param {object} [options] - The set name/value pairs that can be sent as request parameters to control client and/or server side request processing
421              * */
422             push: function (source, event, options) {
423                 var entry = new QueueEntry(this, source, event, options);
424                 var requestGroupingId = entry.getRequestGroupId();
425 
426                 var lastEntry = getLastEntry();
427 
428                 if (lastEntry) {
429                     if (lastEntry.getRequestGroupId() == requestGroupingId) {
430                         rf.log.debug("Similar request currently in queue");
431 
432                         rf.log.debug("Combine similar requests and reset timer");
433 
434                         lastEntry.stopTimer();
435                         entry.setEventsCount(lastEntry.getEventsCount() + 1);
436 
437                         updateLastEntry(entry);
438                         callEventHandler("onrequestqueue", entry);
439                     } else {
440                         rf.log.debug("Last queue entry is not the last anymore. Stopping requestDelay timer and marking entry as ready for submission")
441 
442                         lastEntry.stopTimer();
443                         lastEntry.resetRequestGroupId();
444                         lastEntry.setReadyToSubmit(true);
445 
446                         pushEntry(entry);
447                         submitFirstEntry();
448                     }
449                 } else {
450                     pushEntry(entry);
451                 }
452 
453                 // start timer
454                 entry.startTimer();
455 
456             },
457 
458             response: function (request, context) {
459                 if (this.isIgnoreResponse()) {
460                     lastRequestedEntry = null;
461                     submitFirstEntry();
462                 } else {
463                     rf.ajaxContainer.jsfResponse(request, context);
464                 }
465             },
466 
467             isIgnoreResponse: function () {
468                 var entry = items[0];
469                 return entry && lastRequestedEntry.isIgnoreDupResponses()
470                     && lastRequestedEntry.queueOptions.requestGroupingId == entry.queueOptions.requestGroupingId;
471             },
472 
473             /**
474              * Remove all QueueEntry from the queue
475              * @function
476              * @name RichFaces.queue.clear
477              * */
478             clear: function () {
479                 var lastEntry = getLastEntry();
480                 if (lastEntry) {
481                     lastEntry.stopTimer();
482                 }
483                 items = [];
484             },
485 
486             /**
487              * Set queue default options
488              * @function
489              * @name RichFaces.queue.setQueueOptions
490              *
491              * @param {string||object} [id] - Queue id for storing options or hash with options for multiple options set
492              * @param {object} options - Queue options object
493              * */
494             setQueueOptions: function (id, options) {
495                 var tid = typeof id;
496                 if (tid == "string") {
497                     // add named queue options
498                     if (defaultQueueOptions[id]) {
499                         throw "Queue already registered";
500                     } else {
501                         defaultQueueOptions[id] = options;
502                     }
503                 } else if (tid == "object") {
504                     // first parameter is hash with queue names and options
505                     $.extend(defaultQueueOptions, id);
506                 }
507                 return rf.queue;
508             },
509 
510             getQueueOptions: function (id) {
511                 return defaultQueueOptions[id] || {};
512             }
513         }
514     }());
515 }(RichFaces.jQuery, RichFaces, jsf));
516