Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / direct / RemotingProvider.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.direct.RemotingProvider
17  * @extends Ext.direct.JsonProvider
18  * 
19  * <p>The {@link Ext.direct.RemotingProvider RemotingProvider} exposes access to
20  * server side methods on the client (a remote procedure call (RPC) type of
21  * connection where the client can initiate a procedure on the server).</p>
22  * 
23  * <p>This allows for code to be organized in a fashion that is maintainable,
24  * while providing a clear path between client and server, something that is
25  * not always apparent when using URLs.</p>
26  * 
27  * <p>To accomplish this the server-side needs to describe what classes and methods
28  * are available on the client-side. This configuration will typically be
29  * outputted by the server-side Ext.Direct stack when the API description is built.</p>
30  */
31 Ext.define('Ext.direct.RemotingProvider', {
32     
33     /* Begin Definitions */
34    
35     alias: 'direct.remotingprovider',
36     
37     extend: 'Ext.direct.JsonProvider', 
38     
39     requires: [
40         'Ext.util.MixedCollection', 
41         'Ext.util.DelayedTask', 
42         'Ext.direct.Transaction',
43         'Ext.direct.RemotingMethod'
44     ],
45    
46     /* End Definitions */
47    
48    /**
49      * @cfg {Object} actions
50      * Object literal defining the server side actions and methods. For example, if
51      * the Provider is configured with:
52      * <pre><code>
53 "actions":{ // each property within the 'actions' object represents a server side Class 
54     "TestAction":[ // array of methods within each server side Class to be   
55     {              // stubbed out on client
56         "name":"doEcho", 
57         "len":1            
58     },{
59         "name":"multiply",// name of method
60         "len":2           // The number of parameters that will be used to create an
61                           // array of data to send to the server side function.
62                           // Ensure the server sends back a Number, not a String. 
63     },{
64         "name":"doForm",
65         "formHandler":true, // direct the client to use specialized form handling method 
66         "len":1
67     }]
68 }
69      * </code></pre>
70      * <p>Note that a Store is not required, a server method can be called at any time.
71      * In the following example a <b>client side</b> handler is used to call the
72      * server side method "multiply" in the server-side "TestAction" Class:</p>
73      * <pre><code>
74 TestAction.multiply(
75     2, 4, // pass two arguments to server, so specify len=2
76     // callback function after the server is called
77     // result: the result returned by the server
78     //      e: Ext.direct.RemotingEvent object
79     function(result, e){
80         var t = e.getTransaction();
81         var action = t.action; // server side Class called
82         var method = t.method; // server side method called
83         if(e.status){
84             var answer = Ext.encode(result); // 8
85     
86         }else{
87             var msg = e.message; // failure message
88         }
89     }
90 );
91      * </code></pre>
92      * In the example above, the server side "multiply" function will be passed two
93      * arguments (2 and 4).  The "multiply" method should return the value 8 which will be
94      * available as the <tt>result</tt> in the example above. 
95      */
96     
97     /**
98      * @cfg {String/Object} namespace
99      * Namespace for the Remoting Provider (defaults to the browser global scope of <i>window</i>).
100      * Explicitly specify the namespace Object, or specify a String to have a
101      * {@link Ext#namespace namespace created} implicitly.
102      */
103     
104     /**
105      * @cfg {String} url
106      * <b>Required</b>. The url to connect to the {@link Ext.direct.Manager} server-side router. 
107      */
108     
109     /**
110      * @cfg {String} enableUrlEncode
111      * Specify which param will hold the arguments for the method.
112      * Defaults to <tt>'data'</tt>.
113      */
114     
115     /**
116      * @cfg {Number/Boolean} enableBuffer
117      * <p><tt>true</tt> or <tt>false</tt> to enable or disable combining of method
118      * calls. If a number is specified this is the amount of time in milliseconds
119      * to wait before sending a batched request.</p>
120      * <br><p>Calls which are received within the specified timeframe will be
121      * concatenated together and sent in a single request, optimizing the
122      * application by reducing the amount of round trips that have to be made
123      * to the server.</p>
124      */
125     enableBuffer: 10,
126     
127     /**
128      * @cfg {Number} maxRetries
129      * Number of times to re-attempt delivery on failure of a call.
130      */
131     maxRetries: 1,
132     
133     /**
134      * @cfg {Number} timeout
135      * The timeout to use for each request.
136      */
137     timeout: undefined,
138     
139     constructor : function(config){
140         var me = this;
141         me.callParent(arguments);
142         me.addEvents(
143             /**
144              * @event beforecall
145              * Fires immediately before the client-side sends off the RPC call.
146              * By returning false from an event handler you can prevent the call from
147              * executing.
148              * @param {Ext.direct.RemotingProvider} provider
149              * @param {Ext.direct.Transaction} transaction
150              * @param {Object} meta The meta data
151              */            
152             'beforecall',            
153             /**
154              * @event call
155              * Fires immediately after the request to the server-side is sent. This does
156              * NOT fire after the response has come back from the call.
157              * @param {Ext.direct.RemotingProvider} provider
158              * @param {Ext.direct.Transaction} transaction
159              * @param {Object} meta The meta data
160              */            
161             'call'
162         );
163         me.namespace = (Ext.isString(me.namespace)) ? Ext.ns(me.namespace) : me.namespace || window;
164         me.transactions = Ext.create('Ext.util.MixedCollection');
165         me.callBuffer = [];
166     },
167     
168     /**
169      * Initialize the API
170      * @private
171      */
172     initAPI : function(){
173         var actions = this.actions,
174             namespace = this.namespace,
175             action,
176             cls,
177             methods,
178             i,
179             len,
180             method;
181             
182         for (action in actions) {
183             cls = namespace[action];
184             if (!cls) {
185                 cls = namespace[action] = {};
186             }
187             methods = actions[action];
188             
189             for (i = 0, len = methods.length; i < len; ++i) {
190                 method = Ext.create('Ext.direct.RemotingMethod', methods[i]);
191                 cls[method.name] = this.createHandler(action, method);
192             }
193         }
194     },
195     
196     /**
197      * Create a handler function for a direct call.
198      * @private
199      * @param {String} action The action the call is for
200      * @param {Object} method The details of the method
201      * @return {Function} A JS function that will kick off the call
202      */
203     createHandler : function(action, method){
204         var me = this,
205             handler;
206         
207         if (!method.formHandler) {
208             handler = function(){
209                 me.configureRequest(action, method, Array.prototype.slice.call(arguments, 0));
210             };
211         } else {
212             handler = function(form, callback, scope){
213                 me.configureFormRequest(action, method, form, callback, scope);
214             };
215         }
216         handler.directCfg = {
217             action: action,
218             method: method
219         };
220         return handler;
221     },
222     
223     // inherit docs
224     isConnected: function(){
225         return !!this.connected;
226     },
227
228     // inherit docs
229     connect: function(){
230         var me = this;
231         
232         if (me.url) {
233             me.initAPI();
234             me.connected = true;
235             me.fireEvent('connect', me);
236         } else if(!me.url) {
237             //<debug>
238             Ext.Error.raise('Error initializing RemotingProvider, no url configured.');
239             //</debug>
240         }
241     },
242
243     // inherit docs
244     disconnect: function(){
245         var me = this;
246         
247         if (me.connected) {
248             me.connected = false;
249             me.fireEvent('disconnect', me);
250         }
251     },
252     
253     /**
254      * Run any callbacks related to the transaction.
255      * @private
256      * @param {Ext.direct.Transaction} transaction The transaction
257      * @param {Ext.direct.Event} event The event
258      */
259     runCallback: function(transaction, event){
260         var funcName = event.status ? 'success' : 'failure',
261             callback,
262             result;
263         
264         if (transaction && transaction.callback) {
265             callback = transaction.callback;
266             result = Ext.isDefined(event.result) ? event.result : event.data;
267         
268             if (Ext.isFunction(callback)) {
269                 callback(result, event);
270             } else {
271                 Ext.callback(callback[funcName], callback.scope, [result, event]);
272                 Ext.callback(callback.callback, callback.scope, [result, event]);
273             }
274         }
275     },
276     
277     /**
278      * React to the ajax request being completed
279      * @private
280      */
281     onData: function(options, success, response){
282         var me = this,
283             i = 0,
284             len,
285             events,
286             event,
287             transaction,
288             transactions;
289             
290         if (success) {
291             events = me.createEvents(response);
292             for (len = events.length; i < len; ++i) {
293                 event = events[i];
294                 transaction = me.getTransaction(event);
295                 me.fireEvent('data', me, event);
296                 if (transaction) {
297                     me.runCallback(transaction, event, true);
298                     Ext.direct.Manager.removeTransaction(transaction);
299                 }
300             }
301         } else {
302             transactions = [].concat(options.transaction);
303             for (len = transactions.length; i < len; ++i) {
304                 transaction = me.getTransaction(transactions[i]);
305                 if (transaction && transaction.retryCount < me.maxRetries) {
306                     transaction.retry();
307                 } else {
308                     event = Ext.create('Ext.direct.ExceptionEvent', {
309                         data: null,
310                         transaction: transaction,
311                         code: Ext.direct.Manager.self.exceptions.TRANSPORT,
312                         message: 'Unable to connect to the server.',
313                         xhr: response
314                     });
315                     me.fireEvent('data', me, event);
316                     if (transaction) {
317                         me.runCallback(transaction, event, false);
318                         Ext.direct.Manager.removeTransaction(transaction);
319                     }
320                 }
321             }
322         }
323     },
324     
325     /**
326      * Get transaction from XHR options
327      * @private
328      * @param {Object} options The options sent to the Ajax request
329      * @return {Ext.direct.Transaction} The transaction, null if not found
330      */
331     getTransaction: function(options){
332         return options && options.tid ? Ext.direct.Manager.getTransaction(options.tid) : null;
333     },
334     
335     /**
336      * Configure a direct request
337      * @private
338      * @param {String} action The action being executed
339      * @param {Object} method The being executed
340      */
341     configureRequest: function(action, method, args){
342         var me = this,
343             callData = method.getCallData(args),
344             data = callData.data, 
345             callback = callData.callback, 
346             scope = callData.scope,
347             transaction;
348
349         transaction = Ext.create('Ext.direct.Transaction', {
350             provider: me,
351             args: args,
352             action: action,
353             method: method.name,
354             data: data,
355             callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback
356         });
357
358         if (me.fireEvent('beforecall', me, transaction, method) !== false) {
359             Ext.direct.Manager.addTransaction(transaction);
360             me.queueTransaction(transaction);
361             me.fireEvent('call', me, transaction, method);
362         }
363     },
364     
365     /**
366      * Gets the Ajax call info for a transaction
367      * @private
368      * @param {Ext.direct.Transaction} transaction The transaction
369      * @return {Object} The call params
370      */
371     getCallData: function(transaction){
372         return {
373             action: transaction.action,
374             method: transaction.method,
375             data: transaction.data,
376             type: 'rpc',
377             tid: transaction.id
378         };
379     },
380     
381     /**
382      * Sends a request to the server
383      * @private
384      * @param {Object/Array} data The data to send
385      */
386     sendRequest : function(data){
387         var me = this,
388             request = {
389                 url: me.url,
390                 callback: me.onData,
391                 scope: me,
392                 transaction: data,
393                 timeout: me.timeout
394             }, callData,
395             enableUrlEncode = me.enableUrlEncode,
396             i = 0,
397             len,
398             params;
399             
400
401         if (Ext.isArray(data)) {
402             callData = [];
403             for (len = data.length; i < len; ++i) {
404                 callData.push(me.getCallData(data[i]));
405             }
406         } else {
407             callData = me.getCallData(data);
408         }
409
410         if (enableUrlEncode) {
411             params = {};
412             params[Ext.isString(enableUrlEncode) ? enableUrlEncode : 'data'] = Ext.encode(callData);
413             request.params = params;
414         } else {
415             request.jsonData = callData;
416         }
417         Ext.Ajax.request(request);
418     },
419     
420     /**
421      * Add a new transaction to the queue
422      * @private
423      * @param {Ext.direct.Transaction} transaction The transaction
424      */
425     queueTransaction: function(transaction){
426         var me = this,
427             enableBuffer = me.enableBuffer;
428         
429         if (transaction.form) {
430             me.sendFormRequest(transaction);
431             return;
432         }
433         
434         me.callBuffer.push(transaction);
435         if (enableBuffer) {
436             if (!me.callTask) {
437                 me.callTask = Ext.create('Ext.util.DelayedTask', me.combineAndSend, me);
438             }
439             me.callTask.delay(Ext.isNumber(enableBuffer) ? enableBuffer : 10);
440         } else {
441             me.combineAndSend();
442         }
443     },
444     
445     /**
446      * Combine any buffered requests and send them off
447      * @private
448      */
449     combineAndSend : function(){
450         var buffer = this.callBuffer,
451             len = buffer.length;
452             
453         if (len > 0) {
454             this.sendRequest(len == 1 ? buffer[0] : buffer);
455             this.callBuffer = [];
456         }
457     },
458     
459     /**
460      * Configure a form submission request
461      * @private
462      * @param {String} action The action being executed
463      * @param {Object} method The method being executed
464      * @param {HTMLElement} form The form being submitted
465      * @param {Function} callback (optional) A callback to run after the form submits
466      * @param {Object} scope (optional) A scope to execute the callback in
467      */
468     configureFormRequest : function(action, method, form, callback, scope){
469         var me = this,
470             transaction = Ext.create('Ext.direct.Transaction', {
471                 provider: me,
472                 action: action,
473                 method: method.name,
474                 args: [form, callback, scope],
475                 callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback,
476                 isForm: true
477             }),
478             isUpload,
479             params;
480
481         if (me.fireEvent('beforecall', me, transaction, method) !== false) {
482             Ext.direct.Manager.addTransaction(transaction);
483             isUpload = String(form.getAttribute("enctype")).toLowerCase() == 'multipart/form-data';
484             
485             params = {
486                 extTID: transaction.id,
487                 extAction: action,
488                 extMethod: method.name,
489                 extType: 'rpc',
490                 extUpload: String(isUpload)
491             };
492             
493             // change made from typeof callback check to callback.params
494             // to support addl param passing in DirectSubmit EAC 6/2
495             Ext.apply(transaction, {
496                 form: Ext.getDom(form),
497                 isUpload: isUpload,
498                 params: callback && Ext.isObject(callback.params) ? Ext.apply(params, callback.params) : params
499             });
500             me.fireEvent('call', me, transaction, method);
501             me.sendFormRequest(transaction);
502         }
503     },
504     
505     /**
506      * Sends a form request
507      * @private
508      * @param {Ext.direct.Transaction} transaction The transaction to send
509      */
510     sendFormRequest: function(transaction){
511         Ext.Ajax.request({
512             url: this.url,
513             params: transaction.params,
514             callback: this.onData,
515             scope: this,
516             form: transaction.form,
517             isUpload: transaction.isUpload,
518             transaction: transaction
519         });
520     }
521     
522 });
523