/**
* @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>
*
*/
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,
/**
* Creates new Reader.
* @param {Object} config (optional) Config object.
*/
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, 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
})
});
});