/**
* @author Ed Spencer
*
* Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance or a {@link
* Ext.data.Store Store} - often in response to an AJAX request. In general there is usually no need to create
* a Reader instance directly, since a Reader is almost always used together with a {@link Ext.data.proxy.Proxy Proxy},
* and is configured using the Proxy's {@link Ext.data.proxy.Proxy#cfg-reader reader} configuration property:
*
* Ext.define("User", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id",
* "name"
* ]
* }
* });
*
* Ext.create("Ext.data.Store", {
* model: "User",
* autoLoad: true,
* storeId: "usersStore",
* proxy: {
* type: "ajax",
* url : "users.json",
* reader: {
* type: "json",
* rootProperty: "users"
* }
* }
* });
*
* Ext.create("Ext.List", {
* fullscreen: true,
* itemTpl: "{name} (id: '{id}')",
* store: "usersStore"
* });
*
* The above reader is configured to consume a JSON string that looks something like this:
*
* {
* "success": true,
* "users": [
* { "name": "User 1" },
* { "name": "User 2" }
* ]
* }
*
*
* # Loading Nested Data
*
* Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.association.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:
*
* Ext.define("User", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id",
* "name"
* ],
* hasMany: {
* model: "Order",
* name: "orders"
* },
* proxy: {
* type: "rest",
* url : "users.json",
* reader: {
* type: "json",
* rootProperty: "users"
* }
* }
* }
* });
*
* Ext.define("Order", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id", "total"
* ],
* hasMany: {
* model: "OrderItem",
* name: "orderItems",
* associationKey: "order_items"
* },
* belongsTo: "User"
* }
* });
*
* Ext.define("OrderItem", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id",
* "price",
* "quantity",
* "order_id",
* "product_id"
* ],
* belongsTo: [
* "Order", {
* model: "Product",
* associationKey: "product"
* }
* ]
* }
* });
*
* Ext.define("Product", {
* extend: "Ext.data.Model",
* config: {
* fields: [
* "id",
* "name"
* ]
* },
* hasMany: "OrderItem"
* });
*
* var store = Ext.create('Ext.data.Store', {
* model: "User"
* });
*
* store.load({
* callback: function() {
* var output = [];
*
* // the user that was loaded
* var user = store.first();
*
* output.push("Orders for " + user.get('name') + ":");
*
* // iterate over the Orders for each User
* user.orders().each(function(order) {
* output.push("Order ID: " + order.get('id') + ", 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() method. Usually, we would use the
* // asynchronous version (see Ext.data.association.BelongsTo).
* var product = orderItem.getProduct();
* output.push(orderItem.get("quantity") + " orders of " + product.get("name"));
* });
* });
* Ext.Msg.alert('Output:', output.join("<br/>"));
* }
* });
*
* 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 (_users.json_):
*
* {
* "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"
* }
* }
* ]
* }
* ]
* }
* ]
* }
*
* 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.
*
* Running the code above results in the following:
*
* Orders for Ed:
* Order ID: 50, which contains items:
* 2 orders of MacBook Pro
* 3 orders of iPhone
*/
Ext.define('Ext.data.reader.Reader', {
requires: [
'Ext.data.ResultSet'
],
alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],
mixins: ['Ext.mixin.Observable'],
// @private
isReader: true,
config: {
/**
* @cfg {String} idProperty
* Name of the property within a raw object that contains a record identifier value. Defaults to The id of the
* model. If an `idProperty` is explicitly specified it will override that of the one specified on the model
*/
idProperty: undefined,
/**
* @cfg {String} clientIdProperty
* The name of the property with a response that contains the existing client side id for a record that we are reading.
*/
clientIdProperty: 'clientId',
/**
* @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.
*/
totalProperty: 'total',
/**
* @cfg {String} successProperty
* Name of the property from which to retrieve the success attribute. See
* {@link Ext.data.proxy.Server}.{@link Ext.data.proxy.Server#exception exception} for additional information.
*/
successProperty: 'success',
/**
* @cfg {String} messageProperty (optional)
* The name of the property which contains a response message. This property is optional.
*/
messageProperty: null,
/**
* @cfg {String} rootProperty
* The name of the property which contains the Array of row objects. For JSON reader it's dot-separated list
* of property names. For XML reader it's a CSS selector. For array reader it's not applicable.
*
* By default the natural root of the data will be used. The root JSON array, the root XML element, or the array.
*
* The data packet value for this property should be an empty array to clear the data or show no data.
*/
rootProperty: '',
/**
* @cfg {Boolean} implicitIncludes
* `true` to automatically parse models nested within other models in a response object. See the
* {@link Ext.data.reader.Reader} intro docs for full explanation.
*/
implicitIncludes: true,
model: undefined
},
constructor: function(config) {
this.initConfig(config);
},
/**
* @property {Object} metaData
* The raw meta data that was most recently read, if any. Meta data can include existing
* Reader config options like {@link #idProperty}, {@link #totalProperty}, etc. that get
* automatically applied to the Reader, and those can still be accessed directly from the Reader
* if needed. However, meta data is also often used to pass other custom data to be processed
* by application code. For example, it is common when reconfiguring the data model of a grid to
* also pass a corresponding column model config to be applied to the grid. Any such data will
* not get applied to the Reader directly (it just gets passed through and is ignored by Ext).
* This `metaData` property gives you access to all meta data that was passed, including any such
* custom data ignored by the reader.
*
* This is a read-only property, and it will get replaced each time a new meta data object is
* passed to the reader.
* @readonly
*/
fieldCount: 0,
applyModel: function(model) {
if (typeof model == 'string') {
model = Ext.data.ModelManager.getModel(model);
if (!model) {
Ext.Logger.error('Model with name ' + arguments[0] + ' doesnt exist.');
}
}
if (model && !model.prototype.isModel && Ext.isObject(model)) {
model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
}
return model;
},
applyIdProperty: function(idProperty) {
if (!idProperty && this.getModel()) {
idProperty = this.getModel().getIdProperty();
}
return idProperty;
},
updateModel: function(model) {
if (model) {
if (!this.getIdProperty()) {
this.setIdProperty(model.getIdProperty());
}
this.buildExtractors();
}
},
createAccessor: Ext.emptyFn,
createFieldAccessExpression: function() {
return 'undefined';
},
* @private
* This builds optimized functions for retrieving record data and meta data from an object.
* Subclasses may need to implement their own getRoot function.
*/
buildExtractors: function() {
if (!this.getModel()) {
return;
}
var me = this,
totalProp = me.getTotalProperty(),
successProp = me.getSuccessProperty(),
messageProp = me.getMessageProperty();
//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);
}
me.extractRecordData = me.buildRecordDataExtractor();
},
* @private
* Return a function which will read a raw row object in the format this Reader accepts, and populates
* a record's data object with converted data values.
*
* The returned function must be passed the following parameters:
*
* - `dest` - A record's empty data object into which the new field value properties are injected.
* - `source` - A raw row data object of whatever type this Reader consumes
* - `record - The record which is being populated.
*/
buildRecordDataExtractor: function() {
var me = this,
model = me.getModel(),
fields = model.getFields(),
ln = fields.length,
fieldVarName = [],
clientIdProp = me.getModel().getClientIdProperty(),
prefix = '__field',
code = [
'var me = this,\n',
' fields = me.getModel().getFields(),\n',
' idProperty = me.getIdProperty(),\n',
' idPropertyIsFn = (typeof idProperty == "function"),',
' value,\n',
' internalId'
], i, field, varName, fieldName;
fields = fields.items;
for (i = 0; i < ln; i++) {
field = fields[i];
fieldName = field.getName();
if (fieldName === model.getIdProperty()) {
fieldVarName[i] = 'idField';
} else {
fieldVarName[i] = prefix + i;
}
code.push(',\n ', fieldVarName[i], ' = fields.get("', field.getName(), '")');
}
code.push(';\n\n return function(source) {\n var dest = {};\n');
code.push(' if (idPropertyIsFn) {\n');
code.push(' idField.setMapping(idProperty);\n');
code.push(' }\n');
for (i = 0; i < ln; i++) {
field = fields[i];
varName = fieldVarName[i];
fieldName = field.getName();
if (fieldName === model.getIdProperty() && field.getMapping() === null && model.getIdProperty() !== this.getIdProperty()) {
field.setMapping(this.getIdProperty());
}
// createFieldAccessExpression must be implemented in subclasses to extract data from the source object in the correct way.
code.push(' try {\n');
code.push(' value = ', me.createFieldAccessExpression(field, varName, 'source'), ';\n');
code.push(' if (value !== undefined) {\n');
code.push(' dest["' + field.getName() + '"] = value;\n');
code.push(' }\n');
code.push(' } catch(e){}\n');
}
// set the client id as the internalId of the record.
// clientId handles the case where a client side record did not previously exist on the server,
// so the server is passing back a client id that can be used to pair the server side record up with the client record
if (clientIdProp) {
code.push(' internalId = ' + me.createFieldAccessExpression(Ext.create('Ext.data.Field', {name: clientIdProp}), null, 'source') + ';\n');
code.push(' if (internalId !== undefined) {\n');
code.push(' dest["_clientId"] = internalId;\n }\n');
}
code.push(' return dest;\n');
code.push(' };');
// Here we are creating a new Function and invoking it immediately in the scope of this Reader
// It declares several vars capturing the configured context of this Reader, and returns a function
// which, when passed a record data object, a raw data row in the format this Reader is configured to read,
// and the record which is being created, will populate the record's data object from the raw row data.
return Ext.functionFactory(code.join('')).call(me);
},
getFields: function() {
return this.getModel().getFields().items;
},
/**
* @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;
},
/**
* 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 response object
* @return {Object} The useful data from the response
*/
getResponseData: function(response) {
return response;
},
/**
* @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 'rootProperty' meta data config.
* See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned.
* @param {Object} data The data object
* @return {Object} The same data object
*/
getRoot: function(data) {
return data;
},
/**
* 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,
Model = this.getModel(),
resultSet, records, i, ln, record;
if (response) {
data = this.getResponseData(response);
}
if (data) {
resultSet = this.readRecords(data);
records = resultSet.getRecords();
for (i = 0, ln = records.length; i < ln; i++) {
record = records[i];
records[i] = new Model(record.data, record.id, record.node);
}
return resultSet;
} else {
return this.nullResultSet;
}
},
process: function(response) {
var data = response;
if (response) {
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 {Object} data The raw data object
* @return {Ext.data.ResultSet} A ResultSet object
*/
readRecords: function(data) {
var me = this;
/**
* @property {Object} rawData
* The raw data object that was last passed to readRecords. Stored for further processing if needed
*/
me.rawData = data;
data = me.getData(data);
if (data.metaData) {
me.onMetaChange(data.metaData);
}
// <debug>
if (!me.getModel()) {
Ext.Logger.warn('In order to read record data, a Reader needs to have a Model defined on it.');
}
// </debug>
// If we pass an array as the data, we don't use getRoot on the data.
// Instead the root equals to the data.
var isArray = Ext.isArray(data),
root = isArray ? data : me.getRoot(data),
success = true,
recordCount = 0,
total, value, records, message;
if (isArray && Ext.isEmpty(data.length)) {
return me.nullResultSet;
}
// buildExtractors should have put getTotal, getSuccess, or getMessage methods on the instance.
// So we can check them directly
if (me.getTotal) {
value = parseInt(me.getTotal(data), 10);
if (!isNaN(value)) {
total = value;
}
}
if (me.getSuccess) {
value = me.getSuccess(data);
if (value === false || value === 'false') {
success = false;
}
}
if (me.getMessage) {
message = me.getMessage(data);
}
if (root) {
records = me.extractData(root);
recordCount = records.length;
} else {
recordCount = 0;
records = [];
}
return new Ext.data.ResultSet({
total : total,
count : recordCount,
records: records,
success: success,
message: message
});
},
* Returns extracted, type-cast rows of data.
* @param {Object[]/Object} root from server response
* @private
*/
extractData : function(root) {
var me = this,
records = [],
length = root.length,
model = me.getModel(),
idProperty = model.getIdProperty(),
fieldsCollection = model.getFields(),
node, i, data, id, clientId;
/*
* We check here whether the fields are dirty 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 (fieldsCollection.isDirty) {
me.buildExtractors(true);
delete fieldsCollection.isDirty;
}
if (!root.length && Ext.isObject(root)) {
root = [root];
length = 1;
}
for (i = 0; i < length; i++) {
clientId = null;
id = null;
node = root[i];
// When you use a Memory proxy, and you set data: [] to contain record instances
// this node will already be a record. In this case we should not try to extract
// the record data from the object, but just use the record data attribute.
if (node.isModel) {
data = node.data;
} else {
data = me.extractRecordData(node);
}
if (data._clientId !== undefined) {
clientId = data._clientId;
delete data._clientId;
}
if (data[idProperty] !== undefined) {
id = data[idProperty];
}
if (me.getImplicitIncludes()) {
me.readAssociated(data, node);
}
records.push({
clientId: clientId,
id: id,
data: data,
node: node
});
}
return records;
},
/**
* @private
* Loads a record's associations from the data object. This pre-populates `hasMany` and `belongsTo` associations
* on the record provided.
* @param {Ext.data.Model} record The record to load associations for
* @param {Object} data The data object
*/
readAssociated: function(record, data) {
var associations = this.getModel().associations.items,
length = associations.length,
i = 0,
association, associationData, associationKey;
for (; i < length; i++) {
association = associations[i];
associationKey = association.getAssociationKey();
associationData = this.getAssociatedDataRoot(data, associationKey);
if (associationData) {
record[associationKey] = associationData;
}
}
},
/**
* @private
* Used internally by `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. If a complex
* mapping, this will traverse arrays and objects to resolve the data.
* @param {Object} data The raw data object
* @param {String} associationName The name of the association to get data for (uses associationKey if present)
* @return {Object} The root
*/
getAssociatedDataRoot: function(data, associationName) {
var re = /[\[\.]/,
i = String(associationName).search(re);
if (i >= 0) {
return Ext.functionFactory('obj', 'return obj' + (i > 0 ? '.' : '') + associationName)(data);
}
return data[associationName];
},
/**
* @private
* Reconfigures the meta data tied to this Reader
*/
onMetaChange : function(meta) {
var fields = meta.fields,
me = this,
newModel, config, idProperty;
// save off the raw meta data
me.metaData = meta;
// set any reader-specific configs from meta if available
if (meta.rootProperty !== undefined) {
me.setRootProperty(meta.rootProperty);
}
else if (meta.root !== undefined) {
me.setRootProperty(meta.root);
}
if (meta.idProperty !== undefined) {
me.setIdProperty(meta.idProperty);
}
if (meta.totalProperty !== undefined) {
me.setTotalProperty(meta.totalProperty);
}
if (meta.successProperty !== undefined) {
me.setSuccessProperty(meta.successProperty);
}
if (meta.messageProperty !== undefined) {
me.setMessageProperty(meta.messageProperty);
}
if (fields) {
if (me.getModel()) {
me.getModel().setFields(fields);
me.buildExtractors();
}
else {
idProperty = me.getIdProperty();
config = {fields: fields};
if (idProperty) {
config.idProperty = idProperty;
}
newModel = Ext.define("Ext.data.reader.MetaModel" + Ext.id(), {
extend: 'Ext.data.Model',
config: config
});
me.setModel(newModel);
}
}
else {
me.buildExtractors();
}
}
// Convert old properties in data into a config object
// <deprecated product=touch since=2.0>
,onClassExtended: function(cls, data, hooks) {
var Component = this,
defaultConfig = Component.prototype.config,
config = data.config || {},
key;
for (key in defaultConfig) {
if (key in data) {
config[key] = data[key];
delete data[key];
// <debug warn>
Ext.Logger.deprecate(key + ' is deprecated as a property directly on the Reader prototype. ' +
'Please put it inside the config object.');
// </debug>
}
}
data.config = config;
}
// </deprecated>
}, function() {
Ext.apply(this.prototype, {
// @private
// Empty ResultSet to return when response is falsy (null|undefined|empty string)
nullResultSet: new Ext.data.ResultSet({
total : 0,
count : 0,
records: [],
success: false
})
});
//<deprecated product=touch since=2.0>
/**
* @cfg {String} root
* The name of the property which contains the Array of row objects. For JSON reader it's dot-separated list
* of property names. For XML reader it's a CSS selector. For array reader it's not applicable.
*
* By default the natural root of the data will be used. The root Json array, the root XML element, or the array.
*
* The data packet value for this property should be an empty array to clear the data or show no data.
*
* @deprecated 2.0.0 Please use the {@link #rootProperty} configuration instead.
*/
this.override({
constructor: function(config) {
config = config || {};
if (config.root) {
// <debug>
Ext.Logger.deprecate('root has been deprecated as a configuration on Reader. Please use rootProperty instead.');
// </debug>
config.rootProperty = config.root;
delete config.root;
}
this.callOverridden([config]);
}
});
//</deprecated>
});