/** * The JSON Reader is used by a Proxy to read a server response that is sent back in JSON format. This usually * happens as a result of loading a Store - for example we might create something like this: * * Ext.define('User', { * extend: 'Ext.data.Model', * fields: ['id', 'name', 'email'] * }); * * var store = Ext.create('Ext.data.Store', { * model: 'User', * proxy: { * type: 'ajax', * url : 'users.json', * reader: { * type: 'json' * } * } * }); * * The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're * not already familiar with them. * * We created the simplest type of JSON Reader possible by simply telling our {@link Ext.data.Store Store}'s * {@link Ext.data.proxy.Proxy Proxy} that we want a JSON Reader. The Store automatically passes the configured model to the * Store, so it is as if we passed this instead: * * reader: { * type : 'json', * model: 'User' * } * * The reader we set up is ready to read data from our server - at the moment it will accept a response like this: * * [ * { * "id": 1, * "name": "Ed Spencer", * "email": "ed@sencha.com" * }, * { * "id": 2, * "name": "Abe Elias", * "email": "abe@sencha.com" * } * ] * * ## Reading other JSON formats * * If you already have your JSON format defined and it doesn't look quite like what we have above, you can usually * pass JsonReader a couple of configuration options to make it parse your format. For example, we can use the * {@link #cfg-rootProperty} configuration to parse data that comes back like this: * * { * "users": [ * { * "id": 1, * "name": "Ed Spencer", * "email": "ed@sencha.com" * }, * { * "id": 2, * "name": "Abe Elias", * "email": "abe@sencha.com" * } * ] * } * * To parse this we just pass in a {@link #rootProperty} configuration that matches the 'users' above: * * reader: { * type: 'json', * rootProperty: 'users' * } * * Sometimes the JSON structure is even more complicated. Document databases like CouchDB often provide metadata * around each record inside a nested structure like this: * * { * "total": 122, * "offset": 0, * "users": [ * { * "id": "ed-spencer-1", * "value": 1, * "user": { * "id": 1, * "name": "Ed Spencer", * "email": "ed@sencha.com" * } * } * ] * } * * In the case above the record data is nested an additional level inside the "users" array as each "user" item has * additional metadata surrounding it ('id' and 'value' in this case). To parse data out of each "user" item in the * JSON above we need to specify the {@link #record} configuration like this: * * reader: { * type : 'json', * rootProperty : 'users', * record: 'user' * } * * ## Response MetaData * * The server can return metadata in its response, in addition to the record data, that describe attributes * of the data set itself or are used to reconfigure the Reader. To pass metadata in the response you simply * add a `metaData` attribute to the root of the response data. The metaData attribute can contain anything, * but supports a specific set of properties that are handled by the Reader if they are present: * * - {@link #rootProperty}: the property name of the root response node containing the record data * - {@link #totalProperty}: property name for the total number of records in the data * - {@link #successProperty}: property name for the success status of the response * - {@link #messageProperty}: property name for an optional response message * - {@link Ext.data.Model#cfg-fields fields}: Config used to reconfigure the Model's fields before converting the * response data into records * * An initial Reader configuration containing all of these properties might look like this ("fields" would be * included in the Model definition, not shown): * * reader: { * type : 'json', * rootProperty : 'root', * totalProperty : 'total', * successProperty: 'success', * messageProperty: 'message' * } * * If you were to pass a response object containing attributes different from those initially defined above, you could * use the `metaData` attribute to reconfigure the Reader on the fly. For example: * * { * "count": 1, * "ok": true, * "msg": "Users found", * "users": [{ * "userId": 123, * "name": "Ed Spencer", * "email": "ed@sencha.com" * }], * "metaData": { * "rootProperty": "users", * "totalProperty": 'count', * "successProperty": 'ok', * "messageProperty": 'msg' * } * } * * You can also place any other arbitrary data you need into the `metaData` attribute which will be ignored by the Reader, * but will be accessible via the Reader's {@link #metaData} property (which is also passed to listeners via the Proxy's * {@link Ext.data.proxy.Proxy#metachange metachange} event (also relayed by the store). Application code can then * process the passed metadata in any way it chooses. * * A simple example for how this can be used would be customizing the fields for a Model that is bound to a grid. By passing * the `fields` property the Model will be automatically updated by the Reader internally, but that change will not be * reflected automatically in the grid unless you also update the column configuration. You could do this manually, or you * could simply pass a standard grid {@link Ext.panel.Table#columns column} config object as part of the `metaData` attribute * and then pass that along to the grid. Here's a very simple example for how that could be accomplished: * * // response format: * { * ... * "metaData": { * "fields": [ * { "name": "userId", "type": "int" }, * { "name": "name", "type": "string" }, * { "name": "birthday", "type": "date", "dateFormat": "Y-j-m" }, * ], * "columns": [ * { "text": "User ID", "dataIndex": "userId", "width": 40 }, * { "text": "User Name", "dataIndex": "name", "flex": 1 }, * { "text": "Birthday", "dataIndex": "birthday", "flex": 1, "format": 'Y-j-m', "xtype": "datecolumn" } * ] * } * } * * The Reader will automatically read the meta fields config and rebuild the Model based on the new fields, but to handle * the new column configuration you would need to handle the metadata within the application code. This is done simply enough * by handling the metachange event on either the store or the proxy, e.g.: * * var store = Ext.create('Ext.data.Store', { * ... * listeners: { * 'metachange': function(store, meta) { * myGrid.reconfigure(store, meta.columns); * } * } * }); * */Ext.define('Ext.data.reader.Json', { extend: 'Ext.data.reader.Reader', alternateClassName: 'Ext.data.JsonReader', alias: 'reader.json', requires: [ 'Ext.JSON' ], config: { /** * @cfg {String} record The optional location within the JSON response that the record data itself can be found at. * See the JsonReader intro docs for more details. This is not often needed. */ record: null, /** * @cfg {String} [metaProperty] * Name of the property from which to retrieve the `metaData` attribute. See {@link #metaData}. */ metaProperty: 'metaData', /** * @cfg {Boolean} useSimpleAccessors True to ensure that field names/mappings are treated as literals when * reading values. * * For example, by default, using the mapping "foo.bar.baz" will try and read a property foo from the root, then a property bar * from foo, then a property baz from bar. Setting the simple accessors to true will read the property with the name * "foo.bar.baz" direct from the root object. */ useSimpleAccessors: false, /** * @cfg {Boolean} preserveRawData * The reader will keep a copy of the most recent request in the {@link #rawData} property. For performance reasons, * the data object for each record is used directly as the model data. This means that these objects may be modified and * thus modify the raw data. To ensure the objects are copied, set this option to `true`. NB: This only applies to items * that are read as part of the data array, any other metadata will not be modified: * * { * "someOtherData": 1, // Won't be modified * "root": [{}, {}, {}] // The objects here will be modified * } */ preserveRawData: false }, /** * @private */ responseType: 'json', updateRootProperty: function() { this.forceBuildExtractors(); }, updateMetaProperty: function() { this.forceBuildExtractors(); }, /** * @method readRecords * Reads a JSON object and returns a ResultSet. Uses the internal getTotal and getSuccess extractors to * retrieve meta data from the response, and extractData to turn the JSON data into model instances. * @param {Object} data The raw JSON data * @param {Object} [readOptions] See {@link #read} for details. * @return {Ext.data.ResultSet} A ResultSet containing model instances and meta data about the results */ getResponseData: function(response) { var error; if (typeof response.responseJson === 'object') { return response.responseJson; } try { return Ext.decode(response.responseText); } catch (ex) { error = this.createReadError(ex.message); Ext.Logger.warn('Unable to parse the JSON returned by the server'); this.fireEvent('exception', this, response, error); return error; } }, buildExtractors : function(force) { var me = this, emptyFn = Ext.emptyFn, prop; // Will only return true if we need to build if (me.callParent([force])) { me.getRoot = me.setupExtractor(me.getRootProperty(), Ext.identityFn); me.getGroupRoot = me.setupExtractor(me.getGroupRootProperty(), emptyFn); me.getSummaryRoot = me.setupExtractor(me.getSummaryRootProperty(), emptyFn); me.getMeta = me.setupExtractor(me.getMetaProperty(), emptyFn); } }, /** * @private * We're just preparing the data for the superclass by pulling out the record objects we want. If a {@link #record} * was specified we have to pull those out of the larger JSON object, which is most of what this function is doing * @param {Object} root The JSON root node * @param {Object} [readOptions] See {@link #read} for details. * @return {Ext.data.Model[]} The records */ extractData: function(root, readOptions) { var recordName = this.getRecord(), data = [], length, i; if (recordName) { length = root.length; if (!length && Ext.isObject(root)) { length = 1; root = [root]; } for (i = 0; i < length; i++) { data[i] = root[i][recordName]; } } else { data = root; } return this.callParent([data, readOptions]); }, getModelData: function(raw) { return this.getPreserveRawData() ? Ext.apply({}, raw) : raw; }, /** * @private * @method * Returns an accessor function for the given property string. Gives support for properties such as the following: * * - 'someProperty' * - 'some.property' * - '["someProperty"]' * - 'values[0]' * * This is used by {@link #buildExtractors} to create optimized extractor functions for properties that are looked * up directly on the source object (e.g. {@link #successProperty}, {@link #messageProperty}, etc.). */ createAccessor: (function() { var re = /[\[\.]/; return function(expr) { var simple = this.getUseSimpleAccessors(), operatorIndex, result, current, parts, part, inExpr, isDot, isLeft, isRight, special, c, i, bracketed, len; if (!(expr || expr === 0)) { return; } if (typeof expr === 'function') { return expr; } if (!simple) { operatorIndex = String(expr).search(re); } if (simple === true || operatorIndex < 0) { result = function(raw) { return raw == null ? null : raw[expr]; }; } else { // The purpose of this part is to generate a "safe" accessor for any complex // json expression. For example 'foo.bar.baz' will get transformed: // raw.foo && raw.foo.bar && raw.foo.bar.baz current = 'raw'; parts = []; part = ''; inExpr = 0; len = expr.length; // The <= is intentional here. We handle the last character // being undefined so that we can append any final values at // the end for (i = 0; i <= len; ++i) { c = expr[i]; isDot = c === '.'; isLeft = c === '['; isRight = c === ']'; special = isDot || isLeft || isRight || !c; // If either: // a) Not a special char // b) We're nested more than 1 deep, no single char can bring us out // c) We are in an expr & it's not an ending brace // Then just push the character on if (!special || inExpr > 1 || (inExpr && !isRight)) { part += c; } else if (special) { bracketed = false; if (isLeft) { ++inExpr; } else if (isRight) { --inExpr; bracketed = true; } if (part) { if (bracketed) { part = '[' + part + ']'; } else { part = '.' + part; } current += part; // Concatting the empty string to the start fixes a very odd intermittent bug with IE9/10. // On some occasions, without it, it will end up generating // raw.foo.bar.baz && raw.foo.bar.baz && raw.foo.bar.baz // At this point, not really sure why forcibly casting it to a string makes a difference parts.push('' + current); part = ''; } } } result = parts.join(' && '); result = Ext.functionFactory('raw', 'return ' + result); } return result; }; }()), /** * @private * @method * Returns an accessor function for the passed Field. Gives support for properties such as the following: * * - 'someProperty' * - 'some.property' * - '["someProperty"]' * - 'values[0]' * * This is used by {@link #buildExtractors} to create optimized extractor expressions when converting raw * data into model instances. This method is used at the field level to dynamically map values to model fields. */ createFieldAccessor: function(field) { // Need to capture me for the extractor var me = this, mapping = field.mapping, hasMap = mapping || mapping === 0, map = hasMap ? mapping : field.name; if (hasMap) { if (typeof map === 'function') { return function(raw, self) { return field.mapping(raw, self); }; } else { return me.createAccessor(map); } } }, getAccessorKey: function(prop) { var simple = this.getUseSimpleAccessors() ? 'simple' : ''; return this.callParent([simple + prop]); }, privates: { copyFrom: function(reader) { this.callParent([reader]); this.getRoot = reader.getRoot; }, setupExtractor: function(prop, defaultFn) { return prop ? this.getAccessor(prop) : defaultFn; } }});