Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / data / reader / Reader.js
diff --git a/src/data/reader/Reader.js b/src/data/reader/Reader.js
new file mode 100644 (file)
index 0000000..25ff12f
--- /dev/null
@@ -0,0 +1,586 @@
+/**
+ * @author Ed Spencer
+ * @class Ext.data.reader.Reader
+ * @extends Object
+ * 
+ * <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}
+ * - usually in response to an AJAX request. This is normally handled transparently by passing some configuration to either the 
+ * {@link Ext.data.Model Model} or the {@link Ext.data.Store Store} in question - see their documentation for further details.</p>
+ * 
+ * <p><u>Loading Nested Data</u></p>
+ * 
+ * <p>Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.Association associations}
+ * configured on each Model. Below is an example demonstrating the flexibility of these associations in a fictional CRM system which
+ * manages a User, their Orders, OrderItems and Products. First we'll define the models:
+ * 
+<pre><code>
+Ext.define("User", {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'name'
+    ],
+
+    hasMany: {model: 'Order', name: 'orders'},
+
+    proxy: {
+        type: 'rest',
+        url : 'users.json',
+        reader: {
+            type: 'json',
+            root: 'users'
+        }
+    }
+});
+
+Ext.define("Order", {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'total'
+    ],
+
+    hasMany  : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
+    belongsTo: 'User'
+});
+
+Ext.define("OrderItem", {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'price', 'quantity', 'order_id', 'product_id'
+    ],
+
+    belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
+});
+
+Ext.define("Product", {
+    extend: 'Ext.data.Model',
+    fields: [
+        'id', 'name'
+    ],
+
+    hasMany: 'OrderItem'
+});
+</code></pre>
+ * 
+ * <p>This may be a lot to take in - basically a User has many Orders, each of which is composed of several OrderItems. Finally,
+ * each OrderItem has a single Product. This allows us to consume data like this:</p>
+ * 
+<pre><code>
+{
+    "users": [
+        {
+            "id": 123,
+            "name": "Ed",
+            "orders": [
+                {
+                    "id": 50,
+                    "total": 100,
+                    "order_items": [
+                        {
+                            "id"      : 20,
+                            "price"   : 40,
+                            "quantity": 2,
+                            "product" : {
+                                "id": 1000,
+                                "name": "MacBook Pro"
+                            }
+                        },
+                        {
+                            "id"      : 21,
+                            "price"   : 20,
+                            "quantity": 3,
+                            "product" : {
+                                "id": 1001,
+                                "name": "iPhone"
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    ]
+}
+</code></pre>
+ * 
+ * <p>The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's sake), all of the Orders
+ * for each User (again just 1 in this case), all of the OrderItems for each Order (2 order items in this case), and finally
+ * the Product associated with each OrderItem. Now we can read the data and use it as follows:
+ * 
+<pre><code>
+var store = new Ext.data.Store({
+    model: "User"
+});
+
+store.load({
+    callback: function() {
+        //the user that was loaded
+        var user = store.first();
+
+        console.log("Orders for " + user.get('name') + ":")
+
+        //iterate over the Orders for each User
+        user.orders().each(function(order) {
+            console.log("Order ID: " + order.getId() + ", which contains items:");
+
+            //iterate over the OrderItems for each Order
+            order.orderItems().each(function(orderItem) {
+                //we know that the Product data is already loaded, so we can use the synchronous getProduct
+                //usually, we would use the asynchronous version (see {@link Ext.data.BelongsToAssociation})
+                var product = orderItem.getProduct();
+
+                console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
+            });
+        });
+    }
+});
+</code></pre>
+ * 
+ * <p>Running the code above results in the following:</p>
+ * 
+<pre><code>
+Orders for Ed:
+Order ID: 50, which contains items:
+2 orders of MacBook Pro
+3 orders of iPhone
+</code></pre>
+ * 
+ * @constructor
+ * @param {Object} config Optional config object
+ */
+Ext.define('Ext.data.reader.Reader', {
+    requires: ['Ext.data.ResultSet'],
+    alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],
+    
+    /**
+     * @cfg {String} idProperty Name of the property within a row object
+     * that contains a record identifier value.  Defaults to <tt>The id of the model</tt>.
+     * If an idProperty is explicitly specified it will override that of the one specified
+     * on the model
+     */
+
+    /**
+     * @cfg {String} totalProperty Name of the property from which to
+     * retrieve the total number of records in the dataset. This is only needed
+     * if the whole dataset is not passed in one go, but is being paged from
+     * the remote server.  Defaults to <tt>total</tt>.
+     */
+    totalProperty: 'total',
+
+    /**
+     * @cfg {String} successProperty Name of the property from which to
+     * retrieve the success attribute. Defaults to <tt>success</tt>.  See
+     * {@link Ext.data.proxy.Proxy}.{@link Ext.data.proxy.Proxy#exception exception}
+     * for additional information.
+     */
+    successProperty: 'success',
+
+    /**
+     * @cfg {String} root <b>Required</b>.  The name of the property
+     * which contains the Array of row objects.  Defaults to <tt>undefined</tt>.
+     * An exception will be thrown if the root property is undefined. The data
+     * packet value for this property should be an empty array to clear the data
+     * or show no data.
+     */
+    root: '',
+    
+    /**
+     * @cfg {String} messageProperty The name of the property which contains a response message.
+     * This property is optional.
+     */
+    
+    /**
+     * @cfg {Boolean} implicitIncludes True to automatically parse models nested within other models in a response
+     * object. See the Ext.data.reader.Reader intro docs for full explanation. Defaults to true.
+     */
+    implicitIncludes: true,
+    
+    isReader: true,
+    
+    constructor: function(config) {
+        var me = this;
+        
+        Ext.apply(me, config || {});
+        me.fieldCount = 0;
+        me.model = Ext.ModelManager.getModel(config.model);
+        if (me.model) {
+            me.buildExtractors();
+        }
+    },
+
+    /**
+     * Sets a new model for the reader.
+     * @private
+     * @param {Object} model The model to set.
+     * @param {Boolean} setOnProxy True to also set on the Proxy, if one is configured
+     */
+    setModel: function(model, setOnProxy) {
+        var me = this;
+        
+        me.model = Ext.ModelManager.getModel(model);
+        me.buildExtractors(true);
+        
+        if (setOnProxy && me.proxy) {
+            me.proxy.setModel(me.model, true);
+        }
+    },
+
+    /**
+     * Reads the given response object. This method normalizes the different types of response object that may be passed
+     * to it, before handing off the reading of records to the {@link #readRecords} function.
+     * @param {Object} response The response object. This may be either an XMLHttpRequest object or a plain JS object
+     * @return {Ext.data.ResultSet} The parsed ResultSet object
+     */
+    read: function(response) {
+        var data = response;
+        
+        if (response && response.responseText) {
+            data = this.getResponseData(response);
+        }
+        
+        if (data) {
+            return this.readRecords(data);
+        } else {
+            return this.nullResultSet;
+        }
+    },
+
+    /**
+     * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to call
+     * this function before running its own logic and returning the Ext.data.ResultSet instance. For most
+     * Readers additional processing should not be needed.
+     * @param {Mixed} data The raw data object
+     * @return {Ext.data.ResultSet} A ResultSet object
+     */
+    readRecords: function(data) {
+        var me  = this;
+        
+        /*
+         * We check here whether the number of fields has changed since the last read.
+         * This works around an issue when a Model is used for both a Tree and another
+         * source, because the tree decorates the model with extra fields and it causes
+         * issues because the readers aren't notified.
+         */
+        if (me.fieldCount !== me.getFields().length) {
+            me.buildExtractors(true);
+        }
+        
+        /**
+         * The raw data object that was last passed to readRecords. Stored for further processing if needed
+         * @property rawData
+         * @type Mixed
+         */
+        me.rawData = data;
+
+        data = me.getData(data);
+
+        // If we pass an array as the data, we dont use getRoot on the data.
+        // Instead the root equals to the data.
+        var root    = Ext.isArray(data) ? data : me.getRoot(data),
+            success = true,
+            recordCount = 0,
+            total, value, records, message;
+            
+        if (root) {
+            total = root.length;
+        }
+
+        if (me.totalProperty) {
+            value = parseInt(me.getTotal(data), 10);
+            if (!isNaN(value)) {
+                total = value;
+            }
+        }
+
+        if (me.successProperty) {
+            value = me.getSuccess(data);
+            if (value === false || value === 'false') {
+                success = false;
+            }
+        }
+        
+        if (me.messageProperty) {
+            message = me.getMessage(data);
+        }
+        
+        if (root) {
+            records = me.extractData(root);
+            recordCount = records.length;
+        } else {
+            recordCount = 0;
+            records = [];
+        }
+
+        return Ext.create('Ext.data.ResultSet', {
+            total  : total || recordCount,
+            count  : recordCount,
+            records: records,
+            success: success,
+            message: message
+        });
+    },
+
+    /**
+     * Returns extracted, type-cast rows of data.  Iterates to call #extractValues for each row
+     * @param {Object[]/Object} data-root from server response
+     * @private
+     */
+    extractData : function(root) {
+        var me = this,
+            values  = [],
+            records = [],
+            Model   = me.model,
+            i       = 0,
+            length  = root.length,
+            idProp  = me.getIdProperty(),
+            node, id, record;
+            
+        if (!root.length && Ext.isObject(root)) {
+            root = [root];
+            length = 1;
+        }
+
+        for (; i < length; i++) {
+            node   = root[i];
+            values = me.extractValues(node);
+            id     = me.getId(node);
+
+            
+            record = new Model(values, id);
+            record.raw = node;
+            records.push(record);
+                
+            if (me.implicitIncludes) {
+                me.readAssociated(record, node);
+            }
+        }
+
+        return records;
+    },
+    
+    /**
+     * @private
+     * Loads a record's associations from the data object. This prepopulates hasMany and belongsTo associations
+     * on the record provided.
+     * @param {Ext.data.Model} record The record to load associations for
+     * @param {Mixed} data The data object
+     * @return {String} Return value description
+     */
+    readAssociated: function(record, data) {
+        var associations = record.associations.items,
+            i            = 0,
+            length       = associations.length,
+            association, associationData, proxy, reader;
+        
+        for (; i < length; i++) {
+            association     = associations[i];
+            associationData = this.getAssociatedDataRoot(data, association.associationKey || association.name);
+            
+            if (associationData) {
+                reader = association.getReader();
+                if (!reader) {
+                    proxy = association.associatedModel.proxy;
+                    // if the associated model has a Reader already, use that, otherwise attempt to create a sensible one
+                    if (proxy) {
+                        reader = proxy.getReader();
+                    } else {
+                        reader = new this.constructor({
+                            model: association.associatedName
+                        });
+                    }
+                }
+                association.read(record, reader, associationData);
+            }  
+        }
+    },
+    
+    /**
+     * @private
+     * Used internally by {@link #readAssociated}. Given a data object (which could be json, xml etc) for a specific
+     * record, this should return the relevant part of that data for the given association name. This is only really
+     * needed to support the XML Reader, which has to do a query to get the associated data object
+     * @param {Mixed} data The raw data object
+     * @param {String} associationName The name of the association to get data for (uses associationKey if present)
+     * @return {Mixed} The root
+     */
+    getAssociatedDataRoot: function(data, associationName) {
+        return data[associationName];
+    },
+    
+    getFields: function() {
+        return this.model.prototype.fields.items;
+    },
+
+    /**
+     * @private
+     * Given an object representing a single model instance's data, iterates over the model's fields and
+     * builds an object with the value for each field.
+     * @param {Object} data The data object to convert
+     * @return {Object} Data object suitable for use with a model constructor
+     */
+    extractValues: function(data) {
+        var fields = this.getFields(),
+            i      = 0,
+            length = fields.length,
+            output = {},
+            field, value;
+
+        for (; i < length; i++) {
+            field = fields[i];
+            value = this.extractorFunctions[i](data);
+
+            output[field.name] = value;
+        }
+
+        return output;
+    },
+
+    /**
+     * @private
+     * By default this function just returns what is passed to it. It can be overridden in a subclass
+     * to return something else. See XmlReader for an example.
+     * @param {Object} data The data object
+     * @return {Object} The normalized data object
+     */
+    getData: function(data) {
+        return data;
+    },
+
+    /**
+     * @private
+     * This will usually need to be implemented in a subclass. Given a generic data object (the type depends on the type
+     * of data we are reading), this function should return the object as configured by the Reader's 'root' meta data config.
+     * See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned.
+     * @param {Mixed} data The data object
+     * @return {Mixed} The same data object
+     */
+    getRoot: function(data) {
+        return data;
+    },
+
+    /**
+     * 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
+     * @param {Object} response The responce object
+     * @return {Object} The useful data from the response
+     */
+    getResponseData: function(response) {
+        //<debug>
+        Ext.Error.raise("getResponseData must be implemented in the Ext.data.reader.Reader subclass");
+        //</debug>
+    },
+
+    /**
+     * @private
+     * Reconfigures the meta data tied to this Reader
+     */
+    onMetaChange : function(meta) {
+        var fields = meta.fields,
+            newModel;
+        
+        Ext.apply(this, meta);
+        
+        if (fields) {
+            newModel = Ext.define("Ext.data.reader.Json-Model" + Ext.id(), {
+                extend: 'Ext.data.Model',
+                fields: fields
+            });
+            this.setModel(newModel, true);
+        } else {
+            this.buildExtractors(true);
+        }
+    },
+    
+    /**
+     * Get the idProperty to use for extracting data
+     * @private
+     * @return {String} The id property
+     */
+    getIdProperty: function(){
+        var prop = this.idProperty;
+        if (Ext.isEmpty(prop)) {
+            prop = this.model.prototype.idProperty;
+        }
+        return prop;
+    },
+
+    /**
+     * @private
+     * This builds optimized functions for retrieving record data and meta data from an object.
+     * Subclasses may need to implement their own getRoot function.
+     * @param {Boolean} force True to automatically remove existing extractor functions first (defaults to false)
+     */
+    buildExtractors: function(force) {
+        var me          = this,
+            idProp      = me.getIdProperty(),
+            totalProp   = me.totalProperty,
+            successProp = me.successProperty,
+            messageProp = me.messageProperty,
+            accessor;
+            
+        if (force === true) {
+            delete me.extractorFunctions;
+        }
+        
+        if (me.extractorFunctions) {
+            return;
+        }   
+
+        //build the extractors for all the meta data
+        if (totalProp) {
+            me.getTotal = me.createAccessor(totalProp);
+        }
+
+        if (successProp) {
+            me.getSuccess = me.createAccessor(successProp);
+        }
+
+        if (messageProp) {
+            me.getMessage = me.createAccessor(messageProp);
+        }
+
+        if (idProp) {
+            accessor = me.createAccessor(idProp);
+
+            me.getId = function(record) {
+                var id = accessor.call(me, record);
+                return (id === undefined || id === '') ? null : id;
+            };
+        } else {
+            me.getId = function() {
+                return null;
+            };
+        }
+        me.buildFieldExtractors();
+    },
+
+    /**
+     * @private
+     */
+    buildFieldExtractors: function() {
+        //now build the extractors for all the fields
+        var me = this,
+            fields = me.getFields(),
+            ln = fields.length,
+            i  = 0,
+            extractorFunctions = [],
+            field, map;
+
+        for (; i < ln; i++) {
+            field = fields[i];
+            map   = (field.mapping !== undefined && field.mapping !== null) ? field.mapping : field.name;
+
+            extractorFunctions.push(me.createAccessor(map));
+        }
+        me.fieldCount = ln;
+
+        me.extractorFunctions = extractorFunctions;
+    }
+}, function() {
+    Ext.apply(this, {
+        // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string)
+        nullResultSet: Ext.create('Ext.data.ResultSet', {
+            total  : 0,
+            count  : 0,
+            records: [],
+            success: true
+        })
+    });
+});
\ No newline at end of file