Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / data / reader / Reader.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  *
18  * Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance or a {@link
19  * Ext.data.Store Store} - often in response to an AJAX request. In general there is usually no need to create
20  * a Reader instance directly, since a Reader is almost always used together with a {@link Ext.data.proxy.Proxy Proxy},
21  * and is configured using the Proxy's {@link Ext.data.proxy.Proxy#cfg-reader reader} configuration property:
22  * 
23  *     Ext.create('Ext.data.Store', {
24  *         model: 'User',
25  *         proxy: {
26  *             type: 'ajax',
27  *             url : 'users.json',
28  *             reader: {
29  *                 type: 'json',
30  *                 root: 'users'
31  *             }
32  *         },
33  *     });
34  *     
35  * The above reader is configured to consume a JSON string that looks something like this:
36  *  
37  *     {
38  *         "success": true,
39  *         "users": [
40  *             { "name": "User 1" },
41  *             { "name": "User 2" }
42  *         ]
43  *     }
44  * 
45  *
46  * # Loading Nested Data
47  *
48  * Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.Association
49  * associations} configured on each Model. Below is an example demonstrating the flexibility of these associations in a
50  * fictional CRM system which manages a User, their Orders, OrderItems and Products. First we'll define the models:
51  *
52  *     Ext.define("User", {
53  *         extend: 'Ext.data.Model',
54  *         fields: [
55  *             'id', 'name'
56  *         ],
57  *
58  *         hasMany: {model: 'Order', name: 'orders'},
59  *
60  *         proxy: {
61  *             type: 'rest',
62  *             url : 'users.json',
63  *             reader: {
64  *                 type: 'json',
65  *                 root: 'users'
66  *             }
67  *         }
68  *     });
69  *
70  *     Ext.define("Order", {
71  *         extend: 'Ext.data.Model',
72  *         fields: [
73  *             'id', 'total'
74  *         ],
75  *
76  *         hasMany  : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
77  *         belongsTo: 'User'
78  *     });
79  *
80  *     Ext.define("OrderItem", {
81  *         extend: 'Ext.data.Model',
82  *         fields: [
83  *             'id', 'price', 'quantity', 'order_id', 'product_id'
84  *         ],
85  *
86  *         belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
87  *     });
88  *
89  *     Ext.define("Product", {
90  *         extend: 'Ext.data.Model',
91  *         fields: [
92  *             'id', 'name'
93  *         ],
94  *
95  *         hasMany: 'OrderItem'
96  *     });
97  *
98  * This may be a lot to take in - basically a User has many Orders, each of which is composed of several OrderItems.
99  * Finally, each OrderItem has a single Product. This allows us to consume data like this:
100  *
101  *     {
102  *         "users": [
103  *             {
104  *                 "id": 123,
105  *                 "name": "Ed",
106  *                 "orders": [
107  *                     {
108  *                         "id": 50,
109  *                         "total": 100,
110  *                         "order_items": [
111  *                             {
112  *                                 "id"      : 20,
113  *                                 "price"   : 40,
114  *                                 "quantity": 2,
115  *                                 "product" : {
116  *                                     "id": 1000,
117  *                                     "name": "MacBook Pro"
118  *                                 }
119  *                             },
120  *                             {
121  *                                 "id"      : 21,
122  *                                 "price"   : 20,
123  *                                 "quantity": 3,
124  *                                 "product" : {
125  *                                     "id": 1001,
126  *                                     "name": "iPhone"
127  *                                 }
128  *                             }
129  *                         ]
130  *                     }
131  *                 ]
132  *             }
133  *         ]
134  *     }
135  *
136  * The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's sake), all of the
137  * Orders for each User (again just 1 in this case), all of the OrderItems for each Order (2 order items in this case),
138  * and finally the Product associated with each OrderItem. Now we can read the data and use it as follows:
139  *
140  *     var store = Ext.create('Ext.data.Store', {
141  *         model: "User"
142  *     });
143  *
144  *     store.load({
145  *         callback: function() {
146  *             //the user that was loaded
147  *             var user = store.first();
148  *
149  *             console.log("Orders for " + user.get('name') + ":")
150  *
151  *             //iterate over the Orders for each User
152  *             user.orders().each(function(order) {
153  *                 console.log("Order ID: " + order.getId() + ", which contains items:");
154  *
155  *                 //iterate over the OrderItems for each Order
156  *                 order.orderItems().each(function(orderItem) {
157  *                     //we know that the Product data is already loaded, so we can use the synchronous getProduct
158  *                     //usually, we would use the asynchronous version (see {@link Ext.data.BelongsToAssociation})
159  *                     var product = orderItem.getProduct();
160  *
161  *                     console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
162  *                 });
163  *             });
164  *         }
165  *     });
166  *
167  * Running the code above results in the following:
168  *
169  *     Orders for Ed:
170  *     Order ID: 50, which contains items:
171  *     2 orders of MacBook Pro
172  *     3 orders of iPhone
173  */
174 Ext.define('Ext.data.reader.Reader', {
175     requires: ['Ext.data.ResultSet'],
176     alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],
177     
178     /**
179      * @cfg {String} idProperty
180      * Name of the property within a row object that contains a record identifier value. Defaults to The id of the
181      * model. If an idProperty is explicitly specified it will override that of the one specified on the model
182      */
183
184     /**
185      * @cfg {String} totalProperty
186      * Name of the property from which to retrieve the total number of records in the dataset. This is only needed if
187      * the whole dataset is not passed in one go, but is being paged from the remote server. Defaults to total.
188      */
189     totalProperty: 'total',
190
191     /**
192      * @cfg {String} successProperty
193      * Name of the property from which to retrieve the success attribute. Defaults to success. See
194      * {@link Ext.data.proxy.Server}.{@link Ext.data.proxy.Server#exception exception} for additional information.
195      */
196     successProperty: 'success',
197
198     /**
199      * @cfg {String} root
200      * The name of the property which contains the Array of row objects.  For JSON reader it's dot-separated list
201      * of property names.  For XML reader it's a CSS selector.  For array reader it's not applicable.
202      * 
203      * By default the natural root of the data will be used.  The root Json array, the root XML element, or the array.
204      *
205      * The data packet value for this property should be an empty array to clear the data or show no data.
206      */
207     root: '',
208     
209     /**
210      * @cfg {String} messageProperty
211      * The name of the property which contains a response message. This property is optional.
212      */
213     
214     /**
215      * @cfg {Boolean} implicitIncludes
216      * True to automatically parse models nested within other models in a response object. See the
217      * Ext.data.reader.Reader intro docs for full explanation. Defaults to true.
218      */
219     implicitIncludes: true,
220     
221     isReader: true,
222     
223     /**
224      * Creates new Reader.
225      * @param {Object} config (optional) Config object.
226      */
227     constructor: function(config) {
228         var me = this;
229         
230         Ext.apply(me, config || {});
231         me.fieldCount = 0;
232         me.model = Ext.ModelManager.getModel(config.model);
233         if (me.model) {
234             me.buildExtractors();
235         }
236     },
237
238     /**
239      * Sets a new model for the reader.
240      * @private
241      * @param {Object} model The model to set.
242      * @param {Boolean} setOnProxy True to also set on the Proxy, if one is configured
243      */
244     setModel: function(model, setOnProxy) {
245         var me = this;
246         
247         me.model = Ext.ModelManager.getModel(model);
248         me.buildExtractors(true);
249         
250         if (setOnProxy && me.proxy) {
251             me.proxy.setModel(me.model, true);
252         }
253     },
254
255     /**
256      * Reads the given response object. This method normalizes the different types of response object that may be passed
257      * to it, before handing off the reading of records to the {@link #readRecords} function.
258      * @param {Object} response The response object. This may be either an XMLHttpRequest object or a plain JS object
259      * @return {Ext.data.ResultSet} The parsed ResultSet object
260      */
261     read: function(response) {
262         var data = response;
263         
264         if (response && response.responseText) {
265             data = this.getResponseData(response);
266         }
267         
268         if (data) {
269             return this.readRecords(data);
270         } else {
271             return this.nullResultSet;
272         }
273     },
274
275     /**
276      * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to call this function
277      * before running its own logic and returning the Ext.data.ResultSet instance. For most Readers additional
278      * processing should not be needed.
279      * @param {Object} data The raw data object
280      * @return {Ext.data.ResultSet} A ResultSet object
281      */
282     readRecords: function(data) {
283         var me  = this;
284         
285         /*
286          * We check here whether the number of fields has changed since the last read.
287          * This works around an issue when a Model is used for both a Tree and another
288          * source, because the tree decorates the model with extra fields and it causes
289          * issues because the readers aren't notified.
290          */
291         if (me.fieldCount !== me.getFields().length) {
292             me.buildExtractors(true);
293         }
294         
295         /**
296          * @property {Object} rawData
297          * The raw data object that was last passed to readRecords. Stored for further processing if needed
298          */
299         me.rawData = data;
300
301         data = me.getData(data);
302
303         // If we pass an array as the data, we dont use getRoot on the data.
304         // Instead the root equals to the data.
305         var root    = Ext.isArray(data) ? data : me.getRoot(data),
306             success = true,
307             recordCount = 0,
308             total, value, records, message;
309             
310         if (root) {
311             total = root.length;
312         }
313
314         if (me.totalProperty) {
315             value = parseInt(me.getTotal(data), 10);
316             if (!isNaN(value)) {
317                 total = value;
318             }
319         }
320
321         if (me.successProperty) {
322             value = me.getSuccess(data);
323             if (value === false || value === 'false') {
324                 success = false;
325             }
326         }
327         
328         if (me.messageProperty) {
329             message = me.getMessage(data);
330         }
331         
332         if (root) {
333             records = me.extractData(root);
334             recordCount = records.length;
335         } else {
336             recordCount = 0;
337             records = [];
338         }
339
340         return Ext.create('Ext.data.ResultSet', {
341             total  : total || recordCount,
342             count  : recordCount,
343             records: records,
344             success: success,
345             message: message
346         });
347     },
348
349     /**
350      * Returns extracted, type-cast rows of data.  Iterates to call #extractValues for each row
351      * @param {Object[]/Object} root from server response
352      * @private
353      */
354     extractData : function(root) {
355         var me = this,
356             values  = [],
357             records = [],
358             Model   = me.model,
359             i       = 0,
360             length  = root.length,
361             idProp  = me.getIdProperty(),
362             node, id, record;
363             
364         if (!root.length && Ext.isObject(root)) {
365             root = [root];
366             length = 1;
367         }
368
369         for (; i < length; i++) {
370             node   = root[i];
371             values = me.extractValues(node);
372             id     = me.getId(node);
373
374             
375             record = new Model(values, id, node);
376             records.push(record);
377                 
378             if (me.implicitIncludes) {
379                 me.readAssociated(record, node);
380             }
381         }
382
383         return records;
384     },
385     
386     /**
387      * @private
388      * Loads a record's associations from the data object. This prepopulates hasMany and belongsTo associations
389      * on the record provided.
390      * @param {Ext.data.Model} record The record to load associations for
391      * @param {Object} data The data object
392      * @return {String} Return value description
393      */
394     readAssociated: function(record, data) {
395         var associations = record.associations.items,
396             i            = 0,
397             length       = associations.length,
398             association, associationData, proxy, reader;
399         
400         for (; i < length; i++) {
401             association     = associations[i];
402             associationData = this.getAssociatedDataRoot(data, association.associationKey || association.name);
403             
404             if (associationData) {
405                 reader = association.getReader();
406                 if (!reader) {
407                     proxy = association.associatedModel.proxy;
408                     // if the associated model has a Reader already, use that, otherwise attempt to create a sensible one
409                     if (proxy) {
410                         reader = proxy.getReader();
411                     } else {
412                         reader = new this.constructor({
413                             model: association.associatedName
414                         });
415                     }
416                 }
417                 association.read(record, reader, associationData);
418             }  
419         }
420     },
421     
422     /**
423      * @private
424      * Used internally by {@link #readAssociated}. Given a data object (which could be json, xml etc) for a specific
425      * record, this should return the relevant part of that data for the given association name. This is only really
426      * needed to support the XML Reader, which has to do a query to get the associated data object
427      * @param {Object} data The raw data object
428      * @param {String} associationName The name of the association to get data for (uses associationKey if present)
429      * @return {Object} The root
430      */
431     getAssociatedDataRoot: function(data, associationName) {
432         return data[associationName];
433     },
434     
435     getFields: function() {
436         return this.model.prototype.fields.items;
437     },
438
439     /**
440      * @private
441      * Given an object representing a single model instance's data, iterates over the model's fields and
442      * builds an object with the value for each field.
443      * @param {Object} data The data object to convert
444      * @return {Object} Data object suitable for use with a model constructor
445      */
446     extractValues: function(data) {
447         var fields = this.getFields(),
448             i      = 0,
449             length = fields.length,
450             output = {},
451             field, value;
452
453         for (; i < length; i++) {
454             field = fields[i];
455             value = this.extractorFunctions[i](data);
456
457             output[field.name] = value;
458         }
459
460         return output;
461     },
462
463     /**
464      * @private
465      * By default this function just returns what is passed to it. It can be overridden in a subclass
466      * to return something else. See XmlReader for an example.
467      * @param {Object} data The data object
468      * @return {Object} The normalized data object
469      */
470     getData: function(data) {
471         return data;
472     },
473
474     /**
475      * @private
476      * This will usually need to be implemented in a subclass. Given a generic data object (the type depends on the type
477      * of data we are reading), this function should return the object as configured by the Reader's 'root' meta data config.
478      * See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned.
479      * @param {Object} data The data object
480      * @return {Object} The same data object
481      */
482     getRoot: function(data) {
483         return data;
484     },
485
486     /**
487      * Takes a raw response object (as passed to this.read) and returns the useful data segment of it. This must be
488      * implemented by each subclass
489      * @param {Object} response The responce object
490      * @return {Object} The useful data from the response
491      */
492     getResponseData: function(response) {
493         //<debug>
494         Ext.Error.raise("getResponseData must be implemented in the Ext.data.reader.Reader subclass");
495         //</debug>
496     },
497
498     /**
499      * @private
500      * Reconfigures the meta data tied to this Reader
501      */
502     onMetaChange : function(meta) {
503         var fields = meta.fields,
504             newModel;
505         
506         Ext.apply(this, meta);
507         
508         if (fields) {
509             newModel = Ext.define("Ext.data.reader.Json-Model" + Ext.id(), {
510                 extend: 'Ext.data.Model',
511                 fields: fields
512             });
513             this.setModel(newModel, true);
514         } else {
515             this.buildExtractors(true);
516         }
517     },
518     
519     /**
520      * Get the idProperty to use for extracting data
521      * @private
522      * @return {String} The id property
523      */
524     getIdProperty: function(){
525         var prop = this.idProperty;
526         if (Ext.isEmpty(prop)) {
527             prop = this.model.prototype.idProperty;
528         }
529         return prop;
530     },
531
532     /**
533      * @private
534      * This builds optimized functions for retrieving record data and meta data from an object.
535      * Subclasses may need to implement their own getRoot function.
536      * @param {Boolean} [force=false] True to automatically remove existing extractor functions first
537      */
538     buildExtractors: function(force) {
539         var me          = this,
540             idProp      = me.getIdProperty(),
541             totalProp   = me.totalProperty,
542             successProp = me.successProperty,
543             messageProp = me.messageProperty,
544             accessor;
545             
546         if (force === true) {
547             delete me.extractorFunctions;
548         }
549         
550         if (me.extractorFunctions) {
551             return;
552         }   
553
554         //build the extractors for all the meta data
555         if (totalProp) {
556             me.getTotal = me.createAccessor(totalProp);
557         }
558
559         if (successProp) {
560             me.getSuccess = me.createAccessor(successProp);
561         }
562
563         if (messageProp) {
564             me.getMessage = me.createAccessor(messageProp);
565         }
566
567         if (idProp) {
568             accessor = me.createAccessor(idProp);
569
570             me.getId = function(record) {
571                 var id = accessor.call(me, record);
572                 return (id === undefined || id === '') ? null : id;
573             };
574         } else {
575             me.getId = function() {
576                 return null;
577             };
578         }
579         me.buildFieldExtractors();
580     },
581
582     /**
583      * @private
584      */
585     buildFieldExtractors: function() {
586         //now build the extractors for all the fields
587         var me = this,
588             fields = me.getFields(),
589             ln = fields.length,
590             i  = 0,
591             extractorFunctions = [],
592             field, map;
593
594         for (; i < ln; i++) {
595             field = fields[i];
596             map   = (field.mapping !== undefined && field.mapping !== null) ? field.mapping : field.name;
597
598             extractorFunctions.push(me.createAccessor(map));
599         }
600         me.fieldCount = ln;
601
602         me.extractorFunctions = extractorFunctions;
603     }
604 }, function() {
605     Ext.apply(this, {
606         // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string)
607         nullResultSet: Ext.create('Ext.data.ResultSet', {
608             total  : 0,
609             count  : 0,
610             records: [],
611             success: true
612         })
613     });
614 });