Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / data / proxy / JsonP.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  * @author Ed Spencer
17  * @class Ext.data.proxy.JsonP
18  * @extends Ext.data.proxy.Server
19  *
20  * <p>JsonPProxy is useful when you need to load data from a domain other than the one your application is running
21  * on. If your application is running on http://domainA.com it cannot use {@link Ext.data.proxy.Ajax Ajax} to load its
22  * data from http://domainB.com because cross-domain ajax requests are prohibited by the browser.</p>
23  *
24  * <p>We can get around this using a JsonPProxy. JsonPProxy injects a &lt;script&gt; tag into the DOM whenever
25  * an AJAX request would usually be made. Let's say we want to load data from http://domainB.com/users - the script tag
26  * that would be injected might look like this:</p>
27  *
28 <pre><code>
29 &lt;script src="http://domainB.com/users?callback=someCallback"&gt;&lt;/script&gt;
30 </code></pre>
31  *
32  * <p>When we inject the tag above, the browser makes a request to that url and includes the response as if it was any
33  * other type of JavaScript include. By passing a callback in the url above, we're telling domainB's server that we
34  * want to be notified when the result comes in and that it should call our callback function with the data it sends
35  * back. So long as the server formats the response to look like this, everything will work:</p>
36  *
37 <pre><code>
38 someCallback({
39     users: [
40         {
41             id: 1,
42             name: "Ed Spencer",
43             email: "ed@sencha.com"
44         }
45     ]
46 });
47 </code></pre>
48  *
49  * <p>As soon as the script finishes loading, the 'someCallback' function that we passed in the url is called with the
50  * JSON object that the server returned.</p>
51  *
52  * <p>JsonPProxy takes care of all of this automatically. It formats the url you pass, adding the callback
53  * parameter automatically. It even creates a temporary callback function, waits for it to be called and then puts
54  * the data into the Proxy making it look just like you loaded it through a normal {@link Ext.data.proxy.Ajax AjaxProxy}.
55  * Here's how we might set that up:</p>
56  *
57 <pre><code>
58 Ext.define('User', {
59     extend: 'Ext.data.Model',
60     fields: ['id', 'name', 'email']
61 });
62
63 var store = new Ext.data.Store({
64     model: 'User',
65     proxy: {
66         type: 'jsonp',
67         url : 'http://domainB.com/users'
68     }
69 });
70
71 store.load();
72 </code></pre>
73  *
74  * <p>That's all we need to do - JsonPProxy takes care of the rest. In this case the Proxy will have injected a
75  * script tag like this:
76  *
77 <pre><code>
78 &lt;script src="http://domainB.com/users?callback=stcCallback001" id="stcScript001"&gt;&lt;/script&gt;
79 </code></pre>
80  *
81  * <p><u>Customization</u></p>
82  *
83  * <p>Most parts of this script tag can be customized using the {@link #callbackParam}, {@link #callbackPrefix} and
84  * {@link #scriptIdPrefix} configurations. For example:
85  *
86 <pre><code>
87 var store = new Ext.data.Store({
88     model: 'User',
89     proxy: {
90         type: 'jsonp',
91         url : 'http://domainB.com/users',
92         callbackParam: 'theCallbackFunction',
93         callbackPrefix: 'ABC',
94         scriptIdPrefix: 'injectedScript'
95     }
96 });
97
98 store.load();
99 </code></pre>
100  *
101  * <p>Would inject a script tag like this:</p>
102  *
103 <pre><code>
104 &lt;script src="http://domainB.com/users?theCallbackFunction=ABC001" id="injectedScript001"&gt;&lt;/script&gt;
105 </code></pre>
106  *
107  * <p><u>Implementing on the server side</u></p>
108  *
109  * <p>The remote server side needs to be configured to return data in this format. Here are suggestions for how you
110  * might achieve this using Java, PHP and ASP.net:</p>
111  *
112  * <p>Java:</p>
113  *
114 <pre><code>
115 boolean jsonP = false;
116 String cb = request.getParameter("callback");
117 if (cb != null) {
118     jsonP = true;
119     response.setContentType("text/javascript");
120 } else {
121     response.setContentType("application/x-json");
122 }
123 Writer out = response.getWriter();
124 if (jsonP) {
125     out.write(cb + "(");
126 }
127 out.print(dataBlock.toJsonString());
128 if (jsonP) {
129     out.write(");");
130 }
131 </code></pre>
132  *
133  * <p>PHP:</p>
134  *
135 <pre><code>
136 $callback = $_REQUEST['callback'];
137
138 // Create the output object.
139 $output = array('a' => 'Apple', 'b' => 'Banana');
140
141 //start output
142 if ($callback) {
143     header('Content-Type: text/javascript');
144     echo $callback . '(' . json_encode($output) . ');';
145 } else {
146     header('Content-Type: application/x-json');
147     echo json_encode($output);
148 }
149 </code></pre>
150  *
151  * <p>ASP.net:</p>
152  *
153 <pre><code>
154 String jsonString = "{success: true}";
155 String cb = Request.Params.Get("callback");
156 String responseString = "";
157 if (!String.IsNullOrEmpty(cb)) {
158     responseString = cb + "(" + jsonString + ")";
159 } else {
160     responseString = jsonString;
161 }
162 Response.Write(responseString);
163 </code></pre>
164  *
165  */
166 Ext.define('Ext.data.proxy.JsonP', {
167     extend: 'Ext.data.proxy.Server',
168     alternateClassName: 'Ext.data.ScriptTagProxy',
169     alias: ['proxy.jsonp', 'proxy.scripttag'],
170     requires: ['Ext.data.JsonP'],
171
172     defaultWriterType: 'base',
173
174     /**
175      * @cfg {String} callbackKey (Optional) See {@link Ext.data.JsonP#callbackKey}.
176      */
177     callbackKey : 'callback',
178
179     /**
180      * @cfg {String} recordParam
181      * The param name to use when passing records to the server (e.g. 'records=someEncodedRecordString').
182      * Defaults to 'records'
183      */
184     recordParam: 'records',
185
186     /**
187      * @cfg {Boolean} autoAppendParams True to automatically append the request's params to the generated url. Defaults to true
188      */
189     autoAppendParams: true,
190
191     constructor: function(){
192         this.addEvents(
193             /**
194              * @event exception
195              * Fires when the server returns an exception
196              * @param {Ext.data.proxy.Proxy} this
197              * @param {Ext.data.Request} request The request that was sent
198              * @param {Ext.data.Operation} operation The operation that triggered the request
199              */
200             'exception'
201         );
202         this.callParent(arguments);
203     },
204
205     /**
206      * @private
207      * Performs the read request to the remote domain. JsonPProxy does not actually create an Ajax request,
208      * instead we write out a <script> tag based on the configuration of the internal Ext.data.Request object
209      * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute
210      * @param {Function} callback A callback function to execute when the Operation has been completed
211      * @param {Object} scope The scope to execute the callback in
212      */
213     doRequest: function(operation, callback, scope) {
214         //generate the unique IDs for this request
215         var me      = this,
216             writer  = me.getWriter(),
217             request = me.buildRequest(operation),
218             params = request.params;
219
220         if (operation.allowWrite()) {
221             request = writer.write(request);
222         }
223
224         //apply JsonPProxy-specific attributes to the Request
225         Ext.apply(request, {
226             callbackKey: me.callbackKey,
227             timeout: me.timeout,
228             scope: me,
229             disableCaching: false, // handled by the proxy
230             callback: me.createRequestCallback(request, operation, callback, scope)
231         });
232         
233         // prevent doubling up
234         if (me.autoAppendParams) {
235             request.params = {};
236         }
237         
238         request.jsonp = Ext.data.JsonP.request(request);
239         // restore on the request
240         request.params = params;
241         operation.setStarted();
242         me.lastRequest = request;
243
244         return request;
245     },
246
247     /**
248      * @private
249      * Creates and returns the function that is called when the request has completed. The returned function
250      * should accept a Response object, which contains the response to be read by the configured Reader.
251      * The third argument is the callback that should be called after the request has been completed and the Reader has decoded
252      * the response. This callback will typically be the callback passed by a store, e.g. in proxy.read(operation, theCallback, scope)
253      * theCallback refers to the callback argument received by this function.
254      * See {@link #doRequest} for details.
255      * @param {Ext.data.Request} request The Request object
256      * @param {Ext.data.Operation} operation The Operation being executed
257      * @param {Function} callback The callback function to be called when the request completes. This is usually the callback
258      * passed to doRequest
259      * @param {Object} scope The scope in which to execute the callback function
260      * @return {Function} The callback function
261      */
262     createRequestCallback: function(request, operation, callback, scope) {
263         var me = this;
264
265         return function(success, response, errorType) {
266             delete me.lastRequest;
267             me.processResponse(success, operation, request, response, callback, scope);
268         };
269     },
270     
271     // inherit docs
272     setException: function(operation, response) {
273         operation.setException(operation.request.jsonp.errorType);
274     },
275
276
277     /**
278      * Generates a url based on a given Ext.data.Request object. Adds the params and callback function name to the url
279      * @param {Ext.data.Request} request The request object
280      * @return {String} The url
281      */
282     buildUrl: function(request) {
283         var me      = this,
284             url     = me.callParent(arguments),
285             params  = Ext.apply({}, request.params),
286             filters = params.filters,
287             records,
288             filter, i;
289
290         delete params.filters;
291  
292         if (me.autoAppendParams) {
293             url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
294         }
295
296         if (filters && filters.length) {
297             for (i = 0; i < filters.length; i++) {
298                 filter = filters[i];
299
300                 if (filter.value) {
301                     url = Ext.urlAppend(url, filter.property + "=" + filter.value);
302                 }
303             }
304         }
305
306         //if there are any records present, append them to the url also
307         records = request.records;
308
309         if (Ext.isArray(records) && records.length > 0) {
310             url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.recordParam, me.encodeRecords(records)));
311         }
312
313         return url;
314     },
315
316     //inherit docs
317     destroy: function() {
318         this.abort();
319         this.callParent();
320     },
321
322     /**
323      * Aborts the current server request if one is currently running
324      */
325     abort: function() {
326         var lastRequest = this.lastRequest;
327         if (lastRequest) {
328             Ext.data.JsonP.abort(lastRequest.jsonp);
329         }
330     },
331
332     /**
333      * Encodes an array of records into a string suitable to be appended to the script src url. This is broken
334      * out into its own function so that it can be easily overridden.
335      * @param {Array} records The records array
336      * @return {String} The encoded records string
337      */
338     encodeRecords: function(records) {
339         var encoded = "",
340             i = 0,
341             len = records.length;
342
343         for (; i < len; i++) {
344             encoded += Ext.Object.toQueryString(records[i].data);
345         }
346
347         return encoded;
348     }
349 });
350