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