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