Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / data / reader / Json.js
1 /**
2  * @author Ed Spencer
3  * @class Ext.data.reader.Json
4  * @extends Ext.data.reader.Reader
5  * 
6  * <p>The JSON Reader is used by a Proxy to read a server response that is sent back in JSON format. This usually
7  * happens as a result of loading a Store - for example we might create something like this:</p>
8  * 
9 <pre><code>
10 Ext.define('User', {
11     extend: 'Ext.data.Model',
12     fields: ['id', 'name', 'email']
13 });
14
15 var store = new Ext.data.Store({
16     model: 'User',
17     proxy: {
18         type: 'ajax',
19         url : 'users.json',
20         reader: {
21             type: 'json'
22         }
23     }
24 });
25 </code></pre>
26  * 
27  * <p>The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're
28  * not already familiar with them.</p>
29  * 
30  * <p>We created the simplest type of JSON Reader possible by simply telling our {@link Ext.data.Store Store}'s 
31  * {@link Ext.data.proxy.Proxy Proxy} that we want a JSON Reader. The Store automatically passes the configured model to the
32  * Store, so it is as if we passed this instead:
33  * 
34 <pre><code>
35 reader: {
36     type : 'json',
37     model: 'User'
38 }
39 </code></pre>
40  * 
41  * <p>The reader we set up is ready to read data from our server - at the moment it will accept a response like this:</p>
42  * 
43 <pre><code>
44 [
45     {
46         "id": 1,
47         "name": "Ed Spencer",
48         "email": "ed@sencha.com"
49     },
50     {
51         "id": 2,
52         "name": "Abe Elias",
53         "email": "abe@sencha.com"
54     }
55 ]
56 </code></pre>
57  * 
58  * <p><u>Reading other JSON formats</u></p>
59  * 
60  * <p>If you already have your JSON format defined and it doesn't look quite like what we have above, you can usually
61  * pass JsonReader a couple of configuration options to make it parse your format. For example, we can use the 
62  * {@link #root} configuration to parse data that comes back like this:</p>
63  * 
64 <pre><code>
65 {
66     "users": [
67        {
68            "id": 1,
69            "name": "Ed Spencer",
70            "email": "ed@sencha.com"
71        },
72        {
73            "id": 2,
74            "name": "Abe Elias",
75            "email": "abe@sencha.com"
76        }
77     ]
78 }
79 </code></pre>
80  * 
81  * <p>To parse this we just pass in a {@link #root} configuration that matches the 'users' above:</p>
82  * 
83 <pre><code>
84 reader: {
85     type: 'json',
86     root: 'users'
87 }
88 </code></pre>
89  * 
90  * <p>Sometimes the JSON structure is even more complicated. Document databases like CouchDB often provide metadata
91  * around each record inside a nested structure like this:</p>
92  * 
93 <pre><code>
94 {
95     "total": 122,
96     "offset": 0,
97     "users": [
98         {
99             "id": "ed-spencer-1",
100             "value": 1,
101             "user": {
102                 "id": 1,
103                 "name": "Ed Spencer",
104                 "email": "ed@sencha.com"
105             }
106         }
107     ]
108 }
109 </code></pre>
110  * 
111  * <p>In the case above the record data is nested an additional level inside the "users" array as each "user" item has
112  * additional metadata surrounding it ('id' and 'value' in this case). To parse data out of each "user" item in the 
113  * JSON above we need to specify the {@link #record} configuration like this:</p>
114  * 
115 <pre><code>
116 reader: {
117     type  : 'json',
118     root  : 'users',
119     record: 'user'
120 }
121 </code></pre>
122  * 
123  * <p><u>Response metadata</u></p>
124  * 
125  * <p>The server can return additional data in its response, such as the {@link #totalProperty total number of records} 
126  * and the {@link #successProperty success status of the response}. These are typically included in the JSON response
127  * like this:</p>
128  * 
129 <pre><code>
130 {
131     "total": 100,
132     "success": true,
133     "users": [
134         {
135             "id": 1,
136             "name": "Ed Spencer",
137             "email": "ed@sencha.com"
138         }
139     ]
140 }
141 </code></pre>
142  * 
143  * <p>If these properties are present in the JSON response they can be parsed out by the JsonReader and used by the
144  * Store that loaded it. We can set up the names of these properties by specifying a final pair of configuration 
145  * options:</p>
146  * 
147 <pre><code>
148 reader: {
149     type : 'json',
150     root : 'users',
151     totalProperty  : 'total',
152     successProperty: 'success'
153 }
154 </code></pre>
155  * 
156  * <p>These final options are not necessary to make the Reader work, but can be useful when the server needs to report
157  * an error or if it needs to indicate that there is a lot of data available of which only a subset is currently being
158  * returned.</p>
159  */
160 Ext.define('Ext.data.reader.Json', {
161     extend: 'Ext.data.reader.Reader',
162     alternateClassName: 'Ext.data.JsonReader',
163     alias : 'reader.json',
164     
165     root: '',
166     
167     /**
168      * @cfg {String} record The optional location within the JSON response that the record data itself can be found at.
169      * See the JsonReader intro docs for more details. This is not often needed and defaults to undefined.
170      */
171     
172     /**
173      * @cfg {Boolean} useSimpleAccessors True to ensure that field names/mappings are treated as literals when
174      * reading values. Defalts to <tt>false</tt>.
175      * For example, by default, using the mapping "foo.bar.baz" will try and read a property foo from the root, then a property bar
176      * from foo, then a property baz from bar. Setting the simple accessors to true will read the property with the name 
177      * "foo.bar.baz" direct from the root object.
178      */
179     useSimpleAccessors: false,
180     
181     /**
182      * Reads a JSON object and returns a ResultSet. Uses the internal getTotal and getSuccess extractors to
183      * retrieve meta data from the response, and extractData to turn the JSON data into model instances.
184      * @param {Object} data The raw JSON data
185      * @return {Ext.data.ResultSet} A ResultSet containing model instances and meta data about the results
186      */
187     readRecords: function(data) {
188         //this has to be before the call to super because we use the meta data in the superclass readRecords
189         if (data.metaData) {
190             this.onMetaChange(data.metaData);
191         }
192
193         /**
194          * DEPRECATED - will be removed in Ext JS 5.0. This is just a copy of this.rawData - use that instead
195          * @property jsonData
196          * @type Mixed
197          */
198         this.jsonData = data;
199         return this.callParent([data]);
200     },
201
202     //inherit docs
203     getResponseData: function(response) {
204         try {
205             var data = Ext.decode(response.responseText);
206         }
207         catch (ex) {
208             Ext.Error.raise({
209                 response: response,
210                 json: response.responseText,
211                 parseError: ex,
212                 msg: 'Unable to parse the JSON returned by the server: ' + ex.toString()
213             });
214         }
215         //<debug>
216         if (!data) {
217             Ext.Error.raise('JSON object not found');
218         }
219         //</debug>
220
221         return data;
222     },
223
224     //inherit docs
225     buildExtractors : function() {
226         var me = this;
227         
228         me.callParent(arguments);
229
230         if (me.root) {
231             me.getRoot = me.createAccessor(me.root);
232         } else {
233             me.getRoot = function(root) {
234                 return root;
235             };
236         }
237     },
238     
239     /**
240      * @private
241      * We're just preparing the data for the superclass by pulling out the record objects we want. If a {@link #record}
242      * was specified we have to pull those out of the larger JSON object, which is most of what this function is doing
243      * @param {Object} root The JSON root node
244      * @return {Array} The records
245      */
246     extractData: function(root) {
247         var recordName = this.record,
248             data = [],
249             length, i;
250         
251         if (recordName) {
252             length = root.length;
253             
254             for (i = 0; i < length; i++) {
255                 data[i] = root[i][recordName];
256             }
257         } else {
258             data = root;
259         }
260         return this.callParent([data]);
261     },
262
263     /**
264      * @private
265      * Returns an accessor function for the given property string. Gives support for properties such as the following:
266      * 'someProperty'
267      * 'some.property'
268      * 'some["property"]'
269      * This is used by buildExtractors to create optimized extractor functions when casting raw data into model instances.
270      */
271     createAccessor: function() {
272         var re = /[\[\.]/;
273         
274         return function(expr) {
275             if (Ext.isEmpty(expr)) {
276                 return Ext.emptyFn;
277             }
278             if (Ext.isFunction(expr)) {
279                 return expr;
280             }
281             if (this.useSimpleAccessors !== true) {
282                 var i = String(expr).search(re);
283                 if (i >= 0) {
284                     return Ext.functionFactory('obj', 'return obj' + (i > 0 ? '.' : '') + expr);
285                 }
286             }
287             return function(obj) {
288                 return obj[expr];
289             };
290         };
291     }()
292 });