// SpryJSONDataSet.js - version 0.4 - Spry Pre-Release 1.6 // // Copyright (c) 2007. Adobe Systems Incorporated. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name of Adobe Systems Incorporated nor the names of its // contributors may be used to endorse or promote products derived from this // software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. Spry.Data.JSONDataSet = function(dataSetURL, dataSetOptions) { // Call the constructor for our HTTPSourceDataSet base class so that // our base class properties get defined. this.path = ""; this.pathIsObjectOfArrays = false; this.doc = null; this.subPaths = []; this.useParser = false; this.preparseFunc = null; Spry.Data.HTTPSourceDataSet.call(this, dataSetURL, dataSetOptions); // Callers are allowed to pass either a string, an object or an array of // strings and/or objects for the 'subPaths' option, so make sure we normalize // the subPaths value to be an array. var jwType = typeof this.subPaths; if (jwType == "string" || (jwType == "object" && this.subPaths.constructor != Array)) this.subPaths = [ this.subPaths ]; }; // End of Spry.Data.JSONDataSet() constructor. Spry.Data.JSONDataSet.prototype = new Spry.Data.HTTPSourceDataSet(); Spry.Data.JSONDataSet.prototype.constructor = Spry.Data.JSONDataSet; // Override the inherited version of getDataRefStrings() with our // own version that returns the strings memebers we maintain that // may have data references in them. Spry.Data.JSONDataSet.prototype.getDataRefStrings = function() { var strArr = []; if (this.url) strArr.push(this.url); if (this.path) strArr.push(this.path); if (this.requestInfo && this.requestInfo.postData) strArr.push(this.requestInfo.postData); return strArr; }; Spry.Data.JSONDataSet.prototype.getDocument = function() { return this.doc; }; Spry.Data.JSONDataSet.prototype.getPath = function() { return this.path; }; Spry.Data.JSONDataSet.prototype.setPath = function(path) { if (this.path != path) { this.path = path; if (this.dataWasLoaded && this.doc) { this.notifyObservers("onPreLoad"); this.setDataFromDoc(this.doc); } } }; // A recursive method that returns all objects that match the given object path. Spry.Data.JSONDataSet.getMatchingObjects = function(path, jsonObj) { var results = []; if (path && jsonObj) { var prop = ""; var leftOverPath = ""; var offset = path.search(/\./); if (offset != -1) { prop = path.substring(0, offset); leftOverPath = path.substring(offset + 1); } else prop = path; var matches = []; if (prop && typeof jsonObj == "object") { var obj = jsonObj[prop]; var objType = typeof obj; if (objType != undefined && objType != null) { if (obj && objType == "object" && obj.constructor == Array) matches = matches.concat(obj); else matches.push(obj); } } var numMatches = matches.length; if (leftOverPath) { for (var i = 0; i < numMatches; i++) results = results.concat(Spry.Data.JSONDataSet.getMatchingObjects(leftOverPath, matches[i])); } else results = matches; } return results; }; // Flatten the specified object into a row object that can be added // to a record set. Spry.Data.JSONDataSet.flattenObject = function(obj, basicColumnName) { // If obj is a basic type (null, string, boolean, or number), we need // to store it under a column name in our row object. If the caller supplied // a column name, use that, if not use our default "column0". var basicName = basicColumnName ? basicColumnName : "column0"; // If obj is an object, copy its properties into our row object. If obj // is a basic type, then store it in the row under the column name specified // by basicName. var row = new Object; var objType = typeof obj; if (objType == "object") Spry.Data.JSONDataSet.copyProps(row, obj); else row[basicName] = obj; // Make sure we note the original JSON object we used to create // this row. It may be needed if we need to flatten other sub-paths. row.ds_JSONObject = obj; return row; }; // Utility routine for copying properties from one object to another. Spry.Data.JSONDataSet.copyProps = function(dstObj, srcObj, suppressObjProps) { if (srcObj && dstObj) { for (var prop in srcObj) { if (suppressObjProps && typeof srcObj[prop] == "object") continue; dstObj[prop] = srcObj[prop]; } } return dstObj; }; // Given an object created from JSON data, and an object path, find all the // matching objects and flatten them into rows of data. Spry.Data.JSONDataSet.flattenDataIntoRecordSet = function(jsonObj, path, pathIsObjectOfArrays) { var rs = new Object; rs.data = []; rs.dataHash = {}; if (!path) path = ""; var obj = jsonObj; var objType = typeof obj; var basicColName = ""; // Handle the basic non-object data types. if (objType != "object" || !obj) { // JSON has a null data type which we translate // into a data set with no rows. All other data types // translate into a data set with a single row with a // column named "column0" which contains the actual // data. if (obj != null) { var row = new Object; row.column0 = obj; row.ds_RowID = 0; rs.data.push(row); rs.dataHash[row.ds_RowID] = row; } return rs; } var matches = []; if (obj.constructor == Array) { var arrLen = obj.length; // We have a top-level array. If the array is empty, // then there's nothing for us to do. if (arrLen < 1) return rs; // XXX: We are making a big assumption here that all of the // elements within the array are of the same type! // // If the elements are of a basic data type, we create // a row for each element and add it as a row to the data set. var eleType = typeof obj[0]; if (eleType != "object") { for (var i = 0; i < arrLen; i++) { var row = new Object; row.column0 = obj[i]; row.ds_RowID = i; rs.data.push(row); rs.dataHash[row.ds_RowID] = row; } return rs; } // The elements within the array are objects. // // XXX: If they are arrays, bail, because we don't handle // arrays within arrays right now! if (obj[0].constructor == Array) return rs; // We have an array of objects. If we have a path, use it // to fetch the data the user is interested in from each element // in the array and append the results to our matches array. // If no path was specified, just add the element to the matches // array. if (path) { for (var i = 0; i < arrLen; i++) matches = matches.concat(Spry.Data.JSONDataSet.getMatchingObjects(path, obj[i])); } else { for (var i = 0; i < arrLen; i++) matches.push(obj[i]); } } else { // We have a top-level object. If the user has specified a path, // use it to extract out the data they are interested in. If no // path was specified, then just copy if (path) matches = Spry.Data.JSONDataSet.getMatchingObjects(path, obj); else matches.push(obj); } var numMatches = matches.length; if (path && numMatches >= 1 && typeof matches[0] != "object") basicColName = path.replace(/.*\./, ""); if (!pathIsObjectOfArrays) { for (var i = 0; i < numMatches; i++) { var row = Spry.Data.JSONDataSet.flattenObject(matches[i], basicColName, pathIsObjectOfArrays); row.ds_RowID = i; rs.dataHash[i] = row; rs.data.push(row); } } else { // Each object that was matched contains properties that are the column names // of our rows. The value of each property is an array of values for that column. This // means the data for each row is inverted and spread across multiple arrays. We expect arrays of // objects, so run through all of the arrays and build up row objects and add them // to our record set. var rowID = 0; for (var i = 0; i < numMatches; i++) { var obj = matches[i]; var colNames = []; var maxNumRows = 0; for (var propName in obj) { var prop = obj[propName]; var propyType = typeof prop; if (propyType == 'object' && prop.constructor == Array) { colNames.push(propName); maxNumRows = Math.max(maxNumRows, obj[propName].length); } } var numColNames = colNames.length; for (var j = 0; j < maxNumRows; j++) { var row = new Object; for (var k = 0; k < numColNames; k++) { var colName = colNames[k]; row[colName] = obj[colName][j]; } row.ds_RowID = rowID++; rs.dataHash[i] = row; rs.data.push(row); } } } return rs; }; // For each JSON object used to create the rows in the specified recordset, // find the data the matches the specified subPaths, flatten them, and append // them to the rows of the record set. Spry.Data.JSONDataSet.prototype.flattenSubPaths = function(rs, subPaths) { if (!rs || !subPaths) return; var numSubPaths = subPaths.length; if (numSubPaths < 1) return; var data = rs.data; var dataHash = {}; // Convert all of the templated subPaths to object paths with real values. // We also need a "cleaned" version of the object path which contains no // expressions in it, so that we can pre-pend it to the column names // of any nested data we find. var pathArray = []; var cleanedPathArray = []; var isObjectOfArraysArr = []; for (var i = 0; i < numSubPaths; i++) { // The elements of the subPaths array can be path strings, // or objects that describe a path with nested sub-paths below // it, so make sure we properly extract out the object path to use. var subPath = subPaths[i]; if (typeof subPath == "object") { isObjectOfArraysArr[i] = subPath.pathIsObjectOfArrays; subPath = subPath.path; } if (!subPath) subPath = ""; // Convert any data references in the object path to real values! pathArray[i] = Spry.Data.Region.processDataRefString(null, subPath, this.dataSetsForDataRefStrings); // Create a clean version of the object path by stripping out any // expressions it may contain. cleanedPathArray[i] = pathArray[i].replace(/\[.*\]/g, ""); } // For each row of the base record set passed in, generate a flattened // recordset from each subPath, and then join the results with the base // row. The row from the base data set will be duplicated to match the // number of rows matched by the subPath. The results are then merged. var row; var numRows = data.length; var newData = []; // Iterate over each row of the base record set. for (var i = 0; i < numRows; i++) { row = data[i]; var newRows = [ row ]; // Iterate over every subPath passed into this function. for (var j = 0; j < numSubPaths; j++) { // Search for all nodes that match the given path underneath // the JSON Object for the base row and flatten the data into // a tabular recordset structure. var newRS = Spry.Data.JSONDataSet.flattenDataIntoRecordSet(row.ds_JSONObject, pathArray[j], isObjectOfArraysArr[j]); // If this subPath has additional subPaths beneath it, // flatten and join them with the recordset we just created. if (newRS && newRS.data && newRS.data.length) { if (typeof subPaths[j] == "object" && subPaths[j].subPaths) { // The subPaths property can be either an object path string, // an Object describing a subPath and paths beneath it, // or an Array of object path strings or objects. We need to // normalize these variations into an array to simplify // our processing. var sp = subPaths[j].subPaths; spType = typeof sp; if (spType == "string") sp = [ sp ]; else if (spType == "object" && spType.constructor == Object) sp = [ sp ]; // Now that we have a normalized array of sub paths, flatten // them and join them to the recordSet we just calculated. this.flattenSubPaths(newRS, sp); } var newRSData = newRS.data; var numRSRows = newRSData.length; var cleanedPath = cleanedPathArray[j] + "."; var numNewRows = newRows.length; var joinedRows = []; // Iterate over all rows in our newRows array. Note that the // contents of newRows changes after the execution of this // loop, allowing us to perform more joins when more than // one subPath is specified. for (var k = 0; k < numNewRows; k++) { var newRow = newRows[k]; // Iterate over all rows in the record set generated // from the current subPath. We are going to create // m*n rows for the joined table, where m is the number // of rows in the newRows array, and n is the number of // rows in the current subPath recordset. for (var l = 0; l < numRSRows; l++) { // Create a new row that will house the join result. var newRowObj = new Object; var newRSRow = newRSData[l]; // Copy the data from the current row of the record set // into our new row object, but make sure to store the // data in columns that have the subPath prepended to // it so that it doesn't collide with any columns from // the newRows row data. // Copy the props to the new object using the new property name. for (var prop in newRSRow) { // The new propery name will have the subPath used prepended to it. var newPropName = cleanedPath + prop; // We need to handle the case where the property name of the object matched // by the object path has a value. In that specific case, the name of the // property should be the cleanedPath itself. For example: // // { // "employees": // { // "employee": // [ // "Bob", // "Joe" // ] // } // } // // Object Path: employees.employee // // The property name that contains "Bob" and "Joe" will be "employee". // So in our new row, we need to call this column "employees.employee" // instead of "employees.employee.employee" which would be incorrect. if (cleanedPath == prop || cleanedPath.search(new RegExp("\\." + prop + "\\.$")) != -1) newPropName = cleanedPathArray[j]; // Copy the props to the new object using the new property name. newRowObj[newPropName] = newRSRow[prop]; } // Now copy the columns from the newRow into our row // object. Spry.Data.JSONDataSet.copyProps(newRowObj, newRow); // Now add this row to the array that tracks all of the new // rows we've just created. joinedRows.push(newRowObj); } } // Set the newRows array equal to our joinedRows we just created, // so that when we flatten the data for the next subPath, it gets // joined with our new set of rows. newRows = joinedRows; } } newData = newData.concat(newRows); } // Now that we have a new set of joined rows, we need to run through // all of the rows and make sure they all have a unique row ID and // rebuild our dataHash. data = newData; numRows = data.length; for (i = 0; i < numRows; i++) { row = data[i]; row.ds_RowID = i; dataHash[row.ds_RowID] = row; } // We're all done, so stuff the new data and dataHash // back into the base recordSet. rs.data = data; rs.dataHash = dataHash; }; Spry.Data.JSONDataSet.prototype.parseJSON = function(str, filter) { // The implementation of this JSON Parser is from the json.js (2007-03-20) // reference implementation from json.org. It was modified to accept the // JSON string as an arg, and throw a generic Error. // // Parsing happens in three stages. In the first stage, we run the text against // a regular expression which looks for non-JSON characters. We are especially // concerned with '()' and 'new' because they can cause invocation, and '=' // because it can cause mutation. But just to be safe, we will reject all // unexpected characters. try { if (/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(str)) { // In the second stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. var j = eval('(' + str + ')'); // In the optional third stage, we recursively walk the new structure, passing // each name/value pair to a filter function for possible transformation. if (typeof filter === 'function') { function walk(k, v) { if (v && typeof v === 'object') { for (var i in v) { if (v.hasOwnProperty(i)) { v[i] = walk(i, v[i]); } } } return filter(k, v); } j = walk('', j); } return j; } } catch (e) { // Fall through if the regexp test fails. } throw new Error("Failed to parse JSON string."); }; // Translate the raw JSON string (rawDataDoc) into an object, find the // data within the object we are interested in, and flatten it into // a set of rows for our data set. Spry.Data.JSONDataSet.prototype.loadDataIntoDataSet = function(rawDataDoc) { if (this.preparseFunc) rawDataDoc = this.preparseFunc(this, rawDataDoc); var jsonObj; try { jsonObj = this.useParser ? this.parseJSON(rawDataDoc) : eval("(" + rawDataDoc + ")"); } catch(e) { Spry.Debug.reportError("Caught exception in JSONDataSet.loadDataIntoDataSet: " + e); jsonObj = {}; } if (jsonObj == null) jsonObj = "null"; var rs = Spry.Data.JSONDataSet.flattenDataIntoRecordSet(jsonObj, Spry.Data.Region.processDataRefString(null, this.path, this.dataSetsForDataRefStrings), this.pathIsObjectOfArrays); this.flattenSubPaths(rs, this.subPaths); this.doc = rawDataDoc; this.docObj = jsonObj; this.data = rs.data; this.dataHash = rs.dataHash; this.dataWasLoaded = (this.doc != null); };