Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / src / direct / RemotingProvider.js
1 /*!
2  * Ext JS Library 3.0.0
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**\r
8  * @class Ext.direct.RemotingProvider\r
9  * @extends Ext.direct.JsonProvider\r
10  * \r
11  * <p>The {@link Ext.direct.RemotingProvider RemotingProvider} exposes access to\r
12  * server side methods on the client (a remote procedure call (RPC) type of\r
13  * connection where the client can initiate a procedure on the server).</p>\r
14  * \r
15  * <p>This allows for code to be organized in a fashion that is maintainable,\r
16  * while providing a clear path between client and server, something that is\r
17  * not always apparent when using URLs.</p>\r
18  * \r
19  * <p>To accomplish this the server-side needs to describe what classes and methods\r
20  * are available on the client-side. This configuration will typically be\r
21  * outputted by the server-side Ext.Direct stack when the API description is built.</p>\r
22  */\r
23 Ext.direct.RemotingProvider = Ext.extend(Ext.direct.JsonProvider, {       \r
24     /**\r
25      * @cfg {Object} actions\r
26      * Object literal defining the server side actions and methods. For example, if\r
27      * the Provider is configured with:\r
28      * <pre><code>\r
29 "actions":{ // each property within the 'actions' object represents a server side Class \r
30     "TestAction":[ // array of methods within each server side Class to be   \r
31     {              // stubbed out on client\r
32         "name":"doEcho", \r
33         "len":1            \r
34     },{\r
35         "name":"multiply",// name of method\r
36         "len":2           // The number of parameters that will be used to create an\r
37                           // array of data to send to the server side function.\r
38                           // Ensure the server sends back a Number, not a String. \r
39     },{\r
40         "name":"doForm",\r
41         "formHandler":true, // direct the client to use specialized form handling method \r
42         "len":1\r
43     }]\r
44 }\r
45      * </code></pre>\r
46      * <p>Note that a Store is not required, a server method can be called at any time.\r
47      * In the following example a <b>client side</b> handler is used to call the\r
48      * server side method "multiply" in the server-side "TestAction" Class:</p>\r
49      * <pre><code>\r
50 TestAction.multiply(\r
51     2, 4, // pass two arguments to server, so specify len=2\r
52     // callback function after the server is called\r
53     // result: the result returned by the server\r
54     //      e: Ext.Direct.RemotingEvent object\r
55     function(result, e){\r
56         var t = e.getTransaction();\r
57         var action = t.action; // server side Class called\r
58         var method = t.method; // server side method called\r
59         if(e.status){\r
60             var answer = Ext.encode(result); // 8\r
61     \r
62         }else{\r
63             var msg = e.message; // failure message\r
64         }\r
65     }\r
66 );\r
67      * </code></pre>\r
68      * In the example above, the server side "multiply" function will be passed two\r
69      * arguments (2 and 4).  The "multiply" method should return the value 8 which will be\r
70      * available as the <tt>result</tt> in the example above. \r
71      */\r
72     \r
73     /**\r
74      * @cfg {String/Object} namespace\r
75      * Namespace for the Remoting Provider (defaults to the browser global scope of <i>window</i>).\r
76      * Explicitly specify the namespace Object, or specify a String to have a\r
77      * {@link Ext#namespace namespace created} implicitly.\r
78      */\r
79     \r
80     /**\r
81      * @cfg {String} url\r
82      * <b>Required<b>. The url to connect to the {@link Ext.Direct} server-side router. \r
83      */\r
84     \r
85     /**\r
86      * @cfg {String} enableUrlEncode\r
87      * Specify which param will hold the arguments for the method.\r
88      * Defaults to <tt>'data'</tt>.\r
89      */\r
90     \r
91     /**\r
92      * @cfg {Number/Boolean} enableBuffer\r
93      * <p><tt>true</tt> or <tt>false</tt> to enable or disable combining of method\r
94      * calls. If a number is specified this is the amount of time in milliseconds\r
95      * to wait before sending a batched request (defaults to <tt>10</tt>).</p>\r
96      * <br><p>Calls which are received within the specified timeframe will be\r
97      * concatenated together and sent in a single request, optimizing the\r
98      * application by reducing the amount of round trips that have to be made\r
99      * to the server.</p>\r
100      */\r
101     enableBuffer: 10,\r
102     \r
103     /**\r
104      * @cfg {Number} maxRetries\r
105      * Number of times to re-attempt delivery on failure of a call.\r
106      */\r
107     maxRetries: 1,\r
108 \r
109     constructor : function(config){\r
110         Ext.direct.RemotingProvider.superclass.constructor.call(this, config);\r
111         this.addEvents(\r
112             /**\r
113              * @event beforecall\r
114              * Fires immediately before the client-side sends off the RPC call.\r
115              * By returning false from an event handler you can prevent the call from\r
116              * executing.\r
117              * @param {Ext.direct.RemotingProvider} provider\r
118              * @param {Ext.Direct.Transaction} transaction\r
119              */            \r
120             'beforecall',\r
121             /**\r
122              * @event call\r
123              * Fires immediately after the request to the server-side is sent. This does\r
124              * NOT fire after the response has come back from the call.\r
125              * @param {Ext.direct.RemotingProvider} provider\r
126              * @param {Ext.Direct.Transaction} transaction\r
127              */            \r
128             'call'\r
129         );\r
130         this.namespace = (typeof this.namespace === 'string') ? Ext.ns(this.namespace) : this.namespace || window;\r
131         this.transactions = {};\r
132         this.callBuffer = [];\r
133     },\r
134 \r
135     // private\r
136     initAPI : function(){\r
137         var o = this.actions;\r
138         for(var c in o){\r
139             var cls = this.namespace[c] || (this.namespace[c] = {});\r
140             var ms = o[c];\r
141             for(var i = 0, len = ms.length; i < len; i++){\r
142                 var m = ms[i];\r
143                 cls[m.name] = this.createMethod(c, m);\r
144             }\r
145         }\r
146     },\r
147 \r
148     // inherited\r
149     isConnected: function(){\r
150         return !!this.connected;\r
151     },\r
152 \r
153     connect: function(){\r
154         if(this.url){\r
155             this.initAPI();\r
156             this.connected = true;\r
157             this.fireEvent('connect', this);\r
158         }else if(!this.url){\r
159             throw 'Error initializing RemotingProvider, no url configured.';\r
160         }\r
161     },\r
162 \r
163     disconnect: function(){\r
164         if(this.connected){\r
165             this.connected = false;\r
166             this.fireEvent('disconnect', this);\r
167         }\r
168     },\r
169 \r
170     onData: function(opt, success, xhr){\r
171         if(success){\r
172             var events = this.getEvents(xhr);\r
173             for(var i = 0, len = events.length; i < len; i++){\r
174                 var e = events[i];\r
175                 var t = this.getTransaction(e);\r
176                 this.fireEvent('data', this, e);\r
177                 if(t){\r
178                     this.doCallback(t, e, true);\r
179                     Ext.Direct.removeTransaction(t);\r
180                 }\r
181             }\r
182         }else{\r
183             var ts = [].concat(opt.ts);\r
184             for(var i = 0, len = ts.length; i < len; i++){\r
185                 var t = this.getTransaction(ts[i]);\r
186                 if(t && t.retryCount < this.maxRetries){\r
187                     t.retry();\r
188                 }else{\r
189                     var e = new Ext.Direct.ExceptionEvent({\r
190                         data: e,\r
191                         transaction: t,\r
192                         code: Ext.Direct.exceptions.TRANSPORT,\r
193                         message: 'Unable to connect to the server.',\r
194                         xhr: xhr\r
195                     });\r
196                     this.fireEvent('data', this, e);\r
197                     if(t){\r
198                         this.doCallback(t, e, false);\r
199                         Ext.Direct.removeTransaction(t);\r
200                     }\r
201                 }\r
202             }\r
203         }\r
204     },\r
205 \r
206     getCallData: function(t){\r
207         return {\r
208             action: t.action,\r
209             method: t.method,\r
210             data: t.data,\r
211             type: 'rpc',\r
212             tid: t.tid\r
213         };\r
214     },\r
215 \r
216     doSend : function(data){\r
217         var o = {\r
218             url: this.url,\r
219             callback: this.onData,\r
220             scope: this,\r
221             ts: data\r
222         };\r
223 \r
224         // send only needed data\r
225         var callData;\r
226         if(Ext.isArray(data)){\r
227             callData = [];\r
228             for(var i = 0, len = data.length; i < len; i++){\r
229                 callData.push(this.getCallData(data[i]));\r
230             }\r
231         }else{\r
232             callData = this.getCallData(data);\r
233         }\r
234 \r
235         if(this.enableUrlEncode){\r
236             var params = {};\r
237             params[typeof this.enableUrlEncode == 'string' ? this.enableUrlEncode : 'data'] = Ext.encode(callData);\r
238             o.params = params;\r
239         }else{\r
240             o.jsonData = callData;\r
241         }\r
242         Ext.Ajax.request(o);\r
243     },\r
244 \r
245     combineAndSend : function(){\r
246         var len = this.callBuffer.length;\r
247         if(len > 0){\r
248             this.doSend(len == 1 ? this.callBuffer[0] : this.callBuffer);\r
249             this.callBuffer = [];\r
250         }\r
251     },\r
252 \r
253     queueTransaction: function(t){\r
254         if(t.form){\r
255             this.processForm(t);\r
256             return;\r
257         }\r
258         this.callBuffer.push(t);\r
259         if(this.enableBuffer){\r
260             if(!this.callTask){\r
261                 this.callTask = new Ext.util.DelayedTask(this.combineAndSend, this);\r
262             }\r
263             this.callTask.delay(typeof this.enableBuffer == 'number' ? this.enableBuffer : 10);\r
264         }else{\r
265             this.combineAndSend();\r
266         }\r
267     },\r
268 \r
269     doCall : function(c, m, args){\r
270         var data = null, hs = args[m.len], scope = args[m.len+1];\r
271 \r
272         if(m.len !== 0){\r
273             data = args.slice(0, m.len);\r
274         }\r
275 \r
276         var t = new Ext.Direct.Transaction({\r
277             provider: this,\r
278             args: args,\r
279             action: c,\r
280             method: m.name,\r
281             data: data,\r
282             cb: scope && Ext.isFunction(hs) ? hs.createDelegate(scope) : hs\r
283         });\r
284 \r
285         if(this.fireEvent('beforecall', this, t) !== false){\r
286             Ext.Direct.addTransaction(t);\r
287             this.queueTransaction(t);\r
288             this.fireEvent('call', this, t);\r
289         }\r
290     },\r
291 \r
292     doForm : function(c, m, form, callback, scope){\r
293         var t = new Ext.Direct.Transaction({\r
294             provider: this,\r
295             action: c,\r
296             method: m.name,\r
297             args:[form, callback, scope],\r
298             cb: scope && Ext.isFunction(callback) ? callback.createDelegate(scope) : callback,\r
299             isForm: true\r
300         });\r
301 \r
302         if(this.fireEvent('beforecall', this, t) !== false){\r
303             Ext.Direct.addTransaction(t);\r
304             var isUpload = String(form.getAttribute("enctype")).toLowerCase() == 'multipart/form-data',\r
305                 params = {\r
306                     extTID: t.tid,\r
307                     extAction: c,\r
308                     extMethod: m.name,\r
309                     extType: 'rpc',\r
310                     extUpload: String(isUpload)\r
311                 };\r
312             \r
313             // change made from typeof callback check to callback.params\r
314             // to support addl param passing in DirectSubmit EAC 6/2\r
315             Ext.apply(t, {\r
316                 form: Ext.getDom(form),\r
317                 isUpload: isUpload,\r
318                 params: callback && Ext.isObject(callback.params) ? Ext.apply(params, callback.params) : params\r
319             });\r
320             this.fireEvent('call', this, t);\r
321             this.processForm(t);\r
322         }\r
323     },\r
324     \r
325     processForm: function(t){\r
326         Ext.Ajax.request({\r
327             url: this.url,\r
328             params: t.params,\r
329             callback: this.onData,\r
330             scope: this,\r
331             form: t.form,\r
332             isUpload: t.isUpload,\r
333             ts: t\r
334         });\r
335     },\r
336 \r
337     createMethod : function(c, m){\r
338         var f;\r
339         if(!m.formHandler){\r
340             f = function(){\r
341                 this.doCall(c, m, Array.prototype.slice.call(arguments, 0));\r
342             }.createDelegate(this);\r
343         }else{\r
344             f = function(form, callback, scope){\r
345                 this.doForm(c, m, form, callback, scope);\r
346             }.createDelegate(this);\r
347         }\r
348         f.directCfg = {\r
349             action: c,\r
350             method: m\r
351         };\r
352         return f;\r
353     },\r
354 \r
355     getTransaction: function(opt){\r
356         return opt && opt.tid ? Ext.Direct.getTransaction(opt.tid) : null;\r
357     },\r
358 \r
359     doCallback: function(t, e){\r
360         var fn = e.status ? 'success' : 'failure';\r
361         if(t && t.cb){\r
362             var hs = t.cb;\r
363             var result = e.result || e.data;\r
364             if(Ext.isFunction(hs)){\r
365                 hs(result, e);\r
366             } else{\r
367                 Ext.callback(hs[fn], hs.scope, [result, e]);\r
368                 Ext.callback(hs.callback, hs.scope, [result, e]);\r
369             }\r
370         }\r
371     }\r
372 });\r
373 Ext.Direct.PROVIDERS['remoting'] = Ext.direct.RemotingProvider;