You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
951 lines
33 KiB
951 lines
33 KiB
define(["../_base/kernel", "../_base/lang", "../_base/declare", "../_base/array", "../_base/xhr", |
|
"../Evented", "./util/filter", "./util/simpleFetch", "../date/stamp" |
|
], function(kernel, lang, declare, array, xhr, Evented, filterUtil, simpleFetch, dateStamp){ |
|
|
|
// module: |
|
// dojo/data/ItemFileReadStore |
|
|
|
var ItemFileReadStore = declare("dojo.data.ItemFileReadStore", [Evented],{ |
|
// summary: |
|
// The ItemFileReadStore implements the dojo/data/api/Read API and reads |
|
// data from JSON files that have contents in this format -- |
|
// | { items: [ |
|
// | { name:'Kermit', color:'green', age:12, friends:['Gonzo', {_reference:{name:'Fozzie Bear'}}]}, |
|
// | { name:'Fozzie Bear', wears:['hat', 'tie']}, |
|
// | { name:'Miss Piggy', pets:'Foo-Foo'} |
|
// | ]} |
|
// Note that it can also contain an 'identifier' property that specified which attribute on the items |
|
// in the array of items that acts as the unique identifier for that item. |
|
|
|
constructor: function(/* Object */ keywordParameters){ |
|
// summary: |
|
// constructor |
|
// keywordParameters: |
|
// {url: String} {data: jsonObject} {typeMap: object} |
|
// The structure of the typeMap object is as follows: |
|
// | { |
|
// | type0: function || object, |
|
// | type1: function || object, |
|
// | ... |
|
// | typeN: function || object |
|
// | } |
|
// Where if it is a function, it is assumed to be an object constructor that takes the |
|
// value of _value as the initialization parameters. If it is an object, then it is assumed |
|
// to be an object of general form: |
|
// | { |
|
// | type: function, //constructor. |
|
// | deserialize: function(value) //The function that parses the value and constructs the object defined by type appropriately. |
|
// | } |
|
|
|
this._arrayOfAllItems = []; |
|
this._arrayOfTopLevelItems = []; |
|
this._loadFinished = false; |
|
this._jsonFileUrl = keywordParameters.url; |
|
this._ccUrl = keywordParameters.url; |
|
this.url = keywordParameters.url; |
|
this._jsonData = keywordParameters.data; |
|
this.data = null; |
|
this._datatypeMap = keywordParameters.typeMap || {}; |
|
if(!this._datatypeMap['Date']){ |
|
//If no default mapping for dates, then set this as default. |
|
//We use the dojo/date/stamp here because the ISO format is the 'dojo way' |
|
//of generically representing dates. |
|
this._datatypeMap['Date'] = { |
|
type: Date, |
|
deserialize: function(value){ |
|
return dateStamp.fromISOString(value); |
|
} |
|
}; |
|
} |
|
this._features = {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true}; |
|
this._itemsByIdentity = null; |
|
this._storeRefPropName = "_S"; // Default name for the store reference to attach to every item. |
|
this._itemNumPropName = "_0"; // Default Item Id for isItem to attach to every item. |
|
this._rootItemPropName = "_RI"; // Default Item Id for isItem to attach to every item. |
|
this._reverseRefMap = "_RRM"; // Default attribute for constructing a reverse reference map for use with reference integrity |
|
this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset. |
|
this._queuedFetches = []; |
|
if(keywordParameters.urlPreventCache !== undefined){ |
|
this.urlPreventCache = keywordParameters.urlPreventCache?true:false; |
|
} |
|
if(keywordParameters.hierarchical !== undefined){ |
|
this.hierarchical = keywordParameters.hierarchical?true:false; |
|
} |
|
if(keywordParameters.clearOnClose){ |
|
this.clearOnClose = true; |
|
} |
|
if("failOk" in keywordParameters){ |
|
this.failOk = keywordParameters.failOk?true:false; |
|
} |
|
}, |
|
|
|
url: "", // use "" rather than undefined for the benefit of the parser (#3539) |
|
|
|
//Internal var, crossCheckUrl. Used so that setting either url or _jsonFileUrl, can still trigger a reload |
|
//when clearOnClose and close is used. |
|
_ccUrl: "", |
|
|
|
data: null, // define this so that the parser can populate it |
|
|
|
typeMap: null, //Define so parser can populate. |
|
|
|
// clearOnClose: Boolean |
|
// Parameter to allow users to specify if a close call should force a reload or not. |
|
// By default, it retains the old behavior of not clearing if close is called. But |
|
// if set true, the store will be reset to default state. Note that by doing this, |
|
// all item handles will become invalid and a new fetch must be issued. |
|
clearOnClose: false, |
|
|
|
// urlPreventCache: Boolean |
|
// Parameter to allow specifying if preventCache should be passed to the xhrGet call or not when loading data from a url. |
|
// Note this does not mean the store calls the server on each fetch, only that the data load has preventCache set as an option. |
|
// Added for tracker: #6072 |
|
urlPreventCache: false, |
|
|
|
// failOk: Boolean |
|
// Parameter for specifying that it is OK for the xhrGet call to fail silently. |
|
failOk: false, |
|
|
|
// hierarchical: Boolean |
|
// Parameter to indicate to process data from the url as hierarchical |
|
// (data items can contain other data items in js form). Default is true |
|
// for backwards compatibility. False means only root items are processed |
|
// as items, all child objects outside of type-mapped objects and those in |
|
// specific reference format, are left straight JS data objects. |
|
hierarchical: true, |
|
|
|
_assertIsItem: function(/* dojo/data/api/Item */ item){ |
|
// summary: |
|
// This function tests whether the item passed in is indeed an item in the store. |
|
// item: |
|
// The item to test for being contained by the store. |
|
if(!this.isItem(item)){ |
|
throw new Error(this.declaredClass + ": Invalid item argument."); |
|
} |
|
}, |
|
|
|
_assertIsAttribute: function(/* attribute-name-string */ attribute){ |
|
// summary: |
|
// This function tests whether the item passed in is indeed a valid 'attribute' like type for the store. |
|
// attribute: |
|
// The attribute to test for being contained by the store. |
|
if(typeof attribute !== "string"){ |
|
throw new Error(this.declaredClass + ": Invalid attribute argument."); |
|
} |
|
}, |
|
|
|
getValue: function( /* dojo/data/api/Item */ item, |
|
/* attribute-name-string */ attribute, |
|
/* value? */ defaultValue){ |
|
// summary: |
|
// See dojo/data/api/Read.getValue() |
|
var values = this.getValues(item, attribute); |
|
return (values.length > 0)?values[0]:defaultValue; // mixed |
|
}, |
|
|
|
getValues: function(/* dojo/data/api/Item */ item, |
|
/* attribute-name-string */ attribute){ |
|
// summary: |
|
// See dojo/data/api/Read.getValues() |
|
|
|
this._assertIsItem(item); |
|
this._assertIsAttribute(attribute); |
|
// Clone it before returning. refs: #10474 |
|
return (item[attribute] || []).slice(0); // Array |
|
}, |
|
|
|
getAttributes: function(/* dojo/data/api/Item */ item){ |
|
// summary: |
|
// See dojo/data/api/Read.getAttributes() |
|
this._assertIsItem(item); |
|
var attributes = []; |
|
for(var key in item){ |
|
// Save off only the real item attributes, not the special id marks for O(1) isItem. |
|
if((key !== this._storeRefPropName) && (key !== this._itemNumPropName) && (key !== this._rootItemPropName) && (key !== this._reverseRefMap)){ |
|
attributes.push(key); |
|
} |
|
} |
|
return attributes; // Array |
|
}, |
|
|
|
hasAttribute: function( /* dojo/data/api/Item */ item, |
|
/* attribute-name-string */ attribute){ |
|
// summary: |
|
// See dojo/data/api/Read.hasAttribute() |
|
this._assertIsItem(item); |
|
this._assertIsAttribute(attribute); |
|
return (attribute in item); |
|
}, |
|
|
|
containsValue: function(/* dojo/data/api/Item */ item, |
|
/* attribute-name-string */ attribute, |
|
/* anything */ value){ |
|
// summary: |
|
// See dojo/data/api/Read.containsValue() |
|
var regexp = undefined; |
|
if(typeof value === "string"){ |
|
regexp = filterUtil.patternToRegExp(value, false); |
|
} |
|
return this._containsValue(item, attribute, value, regexp); //boolean. |
|
}, |
|
|
|
_containsValue: function( /* dojo/data/api/Item */ item, |
|
/* attribute-name-string */ attribute, |
|
/* anything */ value, |
|
/* RegExp?*/ regexp){ |
|
// summary: |
|
// Internal function for looking at the values contained by the item. |
|
// description: |
|
// Internal function for looking at the values contained by the item. This |
|
// function allows for denoting if the comparison should be case sensitive for |
|
// strings or not (for handling filtering cases where string case should not matter) |
|
// item: |
|
// The data item to examine for attribute values. |
|
// attribute: |
|
// The attribute to inspect. |
|
// value: |
|
// The value to match. |
|
// regexp: |
|
// Optional regular expression generated off value if value was of string type to handle wildcarding. |
|
// If present and attribute values are string, then it can be used for comparison instead of 'value' |
|
return array.some(this.getValues(item, attribute), function(possibleValue){ |
|
if(possibleValue !== null && !lang.isObject(possibleValue) && regexp){ |
|
if(possibleValue.toString().match(regexp)){ |
|
return true; // Boolean |
|
} |
|
}else if(value === possibleValue){ |
|
return true; // Boolean |
|
} |
|
}); |
|
}, |
|
|
|
isItem: function(/* anything */ something){ |
|
// summary: |
|
// See dojo/data/api/Read.isItem() |
|
if(something && something[this._storeRefPropName] === this){ |
|
if(this._arrayOfAllItems[something[this._itemNumPropName]] === something){ |
|
return true; |
|
} |
|
} |
|
return false; // Boolean |
|
}, |
|
|
|
isItemLoaded: function(/* anything */ something){ |
|
// summary: |
|
// See dojo/data/api/Read.isItemLoaded() |
|
return this.isItem(something); //boolean |
|
}, |
|
|
|
loadItem: function(/* object */ keywordArgs){ |
|
// summary: |
|
// See dojo/data/api/Read.loadItem() |
|
this._assertIsItem(keywordArgs.item); |
|
}, |
|
|
|
getFeatures: function(){ |
|
// summary: |
|
// See dojo/data/api/Read.getFeatures() |
|
return this._features; //Object |
|
}, |
|
|
|
getLabel: function(/* dojo/data/api/Item */ item){ |
|
// summary: |
|
// See dojo/data/api/Read.getLabel() |
|
if(this._labelAttr && this.isItem(item)){ |
|
return this.getValue(item,this._labelAttr); //String |
|
} |
|
return undefined; //undefined |
|
}, |
|
|
|
getLabelAttributes: function(/* dojo/data/api/Item */ item){ |
|
// summary: |
|
// See dojo/data/api/Read.getLabelAttributes() |
|
if(this._labelAttr){ |
|
return [this._labelAttr]; //array |
|
} |
|
return null; //null |
|
}, |
|
|
|
filter: function(/* Object */ requestArgs, /* item[] */ arrayOfItems, /* Function */ findCallback){ |
|
// summary: |
|
// This method handles the basic filtering needs for ItemFile* based stores. |
|
var items = [], |
|
i, key; |
|
|
|
if(requestArgs.query){ |
|
var value, |
|
ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false; |
|
|
|
//See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the |
|
//same value for each item examined. Much more efficient. |
|
var regexpList = {}; |
|
for(key in requestArgs.query){ |
|
value = requestArgs.query[key]; |
|
if(typeof value === "string"){ |
|
regexpList[key] = filterUtil.patternToRegExp(value, ignoreCase); |
|
}else if(value instanceof RegExp){ |
|
regexpList[key] = value; |
|
} |
|
} |
|
for(i = 0; i < arrayOfItems.length; ++i){ |
|
var match = true; |
|
var candidateItem = arrayOfItems[i]; |
|
if(candidateItem === null){ |
|
match = false; |
|
}else{ |
|
for(key in requestArgs.query){ |
|
value = requestArgs.query[key]; |
|
if(!this._containsValue(candidateItem, key, value, regexpList[key])){ |
|
match = false; |
|
} |
|
} |
|
} |
|
if(match){ |
|
items.push(candidateItem); |
|
} |
|
} |
|
findCallback(items, requestArgs); |
|
}else{ |
|
// We want a copy to pass back in case the parent wishes to sort the array. |
|
// We shouldn't allow resort of the internal list, so that multiple callers |
|
// can get lists and sort without affecting each other. We also need to |
|
// filter out any null values that have been left as a result of deleteItem() |
|
// calls in ItemFileWriteStore. |
|
for(i = 0; i < arrayOfItems.length; ++i){ |
|
var item = arrayOfItems[i]; |
|
if(item !== null){ |
|
items.push(item); |
|
} |
|
} |
|
findCallback(items, requestArgs); |
|
} |
|
}, |
|
|
|
_fetchItems: function( /* Object */ keywordArgs, |
|
/* Function */ findCallback, |
|
/* Function */ errorCallback){ |
|
// summary: |
|
// See dojo/data/util.simpleFetch.fetch() |
|
var self = this; |
|
|
|
if(this._loadFinished){ |
|
this.filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions), findCallback); |
|
}else{ |
|
//Do a check on the JsonFileUrl and crosscheck it. |
|
//If it doesn't match the cross-check, it needs to be updated |
|
//This allows for either url or _jsonFileUrl to he changed to |
|
//reset the store load location. Done this way for backwards |
|
//compatibility. People use _jsonFileUrl (even though officially |
|
//private. |
|
if(this._jsonFileUrl !== this._ccUrl){ |
|
kernel.deprecated(this.declaredClass + ": ", |
|
"To change the url, set the url property of the store," + |
|
" not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0"); |
|
this._ccUrl = this._jsonFileUrl; |
|
this.url = this._jsonFileUrl; |
|
}else if(this.url !== this._ccUrl){ |
|
this._jsonFileUrl = this.url; |
|
this._ccUrl = this.url; |
|
} |
|
|
|
//See if there was any forced reset of data. |
|
if(this.data != null){ |
|
this._jsonData = this.data; |
|
this.data = null; |
|
} |
|
|
|
if(this._jsonFileUrl){ |
|
//If fetches come in before the loading has finished, but while |
|
//a load is in progress, we have to defer the fetching to be |
|
//invoked in the callback. |
|
if(this._loadInProgress){ |
|
this._queuedFetches.push({args: keywordArgs, filter: lang.hitch(self, "filter"), findCallback: lang.hitch(self, findCallback)}); |
|
}else{ |
|
this._loadInProgress = true; |
|
var getArgs = { |
|
url: self._jsonFileUrl, |
|
handleAs: "json-comment-optional", |
|
preventCache: this.urlPreventCache, |
|
failOk: this.failOk |
|
}; |
|
var getHandler = xhr.get(getArgs); |
|
getHandler.addCallback(function(data){ |
|
try{ |
|
self._getItemsFromLoadedData(data); |
|
self._loadFinished = true; |
|
self._loadInProgress = false; |
|
|
|
self.filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions), findCallback); |
|
self._handleQueuedFetches(); |
|
}catch(e){ |
|
self._loadFinished = true; |
|
self._loadInProgress = false; |
|
errorCallback(e, keywordArgs); |
|
} |
|
}); |
|
getHandler.addErrback(function(error){ |
|
self._loadInProgress = false; |
|
errorCallback(error, keywordArgs); |
|
}); |
|
|
|
//Wire up the cancel to abort of the request |
|
//This call cancel on the deferred if it hasn't been called |
|
//yet and then will chain to the simple abort of the |
|
//simpleFetch keywordArgs |
|
var oldAbort = null; |
|
if(keywordArgs.abort){ |
|
oldAbort = keywordArgs.abort; |
|
} |
|
keywordArgs.abort = function(){ |
|
var df = getHandler; |
|
if(df && df.fired === -1){ |
|
df.cancel(); |
|
df = null; |
|
} |
|
if(oldAbort){ |
|
oldAbort.call(keywordArgs); |
|
} |
|
}; |
|
} |
|
}else if(this._jsonData){ |
|
try{ |
|
this._loadFinished = true; |
|
this._getItemsFromLoadedData(this._jsonData); |
|
this._jsonData = null; |
|
self.filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions), findCallback); |
|
}catch(e){ |
|
errorCallback(e, keywordArgs); |
|
} |
|
}else{ |
|
errorCallback(new Error(this.declaredClass + ": No JSON source data was provided as either URL or a nested Javascript object."), keywordArgs); |
|
} |
|
} |
|
}, |
|
|
|
_handleQueuedFetches: function(){ |
|
// summary: |
|
// Internal function to execute delayed request in the store. |
|
|
|
//Execute any deferred fetches now. |
|
if(this._queuedFetches.length > 0){ |
|
for(var i = 0; i < this._queuedFetches.length; i++){ |
|
var fData = this._queuedFetches[i], |
|
delayedQuery = fData.args, |
|
delayedFilter = fData.filter, |
|
delayedFindCallback = fData.findCallback; |
|
if(delayedFilter){ |
|
delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions), delayedFindCallback); |
|
}else{ |
|
this.fetchItemByIdentity(delayedQuery); |
|
} |
|
} |
|
this._queuedFetches = []; |
|
} |
|
}, |
|
|
|
_getItemsArray: function(/*object?*/queryOptions){ |
|
// summary: |
|
// Internal function to determine which list of items to search over. |
|
// queryOptions: The query options parameter, if any. |
|
if(queryOptions && queryOptions.deep){ |
|
return this._arrayOfAllItems; |
|
} |
|
return this._arrayOfTopLevelItems; |
|
}, |
|
|
|
close: function(/*dojo/data/api/Request|Object?*/ request){ |
|
// summary: |
|
// See dojo/data/api/Read.close() |
|
if(this.clearOnClose && |
|
this._loadFinished && |
|
!this._loadInProgress){ |
|
//Reset all internalsback to default state. This will force a reload |
|
//on next fetch. This also checks that the data or url param was set |
|
//so that the store knows it can get data. Without one of those being set, |
|
//the next fetch will trigger an error. |
|
|
|
if(((this._jsonFileUrl == "" || this._jsonFileUrl == null) && |
|
(this.url == "" || this.url == null) |
|
) && this.data == null){ |
|
console.debug(this.declaredClass + ": WARNING! Data reload " + |
|
" information has not been provided." + |
|
" Please set 'url' or 'data' to the appropriate value before" + |
|
" the next fetch"); |
|
} |
|
this._arrayOfAllItems = []; |
|
this._arrayOfTopLevelItems = []; |
|
this._loadFinished = false; |
|
this._itemsByIdentity = null; |
|
this._loadInProgress = false; |
|
this._queuedFetches = []; |
|
} |
|
}, |
|
|
|
_getItemsFromLoadedData: function(/* Object */ dataObject){ |
|
// summary: |
|
// Function to parse the loaded data into item format and build the internal items array. |
|
// description: |
|
// Function to parse the loaded data into item format and build the internal items array. |
|
// dataObject: |
|
// The JS data object containing the raw data to convery into item format. |
|
// returns: Array |
|
// Array of items in store item format. |
|
|
|
// First, we define a couple little utility functions... |
|
var addingArrays = false, |
|
self = this; |
|
|
|
function valueIsAnItem(/* anything */ aValue){ |
|
// summary: |
|
// Given any sort of value that could be in the raw json data, |
|
// return true if we should interpret the value as being an |
|
// item itself, rather than a literal value or a reference. |
|
// example: |
|
// | false == valueIsAnItem("Kermit"); |
|
// | false == valueIsAnItem(42); |
|
// | false == valueIsAnItem(new Date()); |
|
// | false == valueIsAnItem({_type:'Date', _value:'1802-05-14'}); |
|
// | false == valueIsAnItem({_reference:'Kermit'}); |
|
// | true == valueIsAnItem({name:'Kermit', color:'green'}); |
|
// | true == valueIsAnItem({iggy:'pop'}); |
|
// | true == valueIsAnItem({foo:42}); |
|
return (aValue !== null) && |
|
(typeof aValue === "object") && |
|
(!lang.isArray(aValue) || addingArrays) && |
|
(!lang.isFunction(aValue)) && |
|
(aValue.constructor == Object || lang.isArray(aValue)) && |
|
(typeof aValue._reference === "undefined") && |
|
(typeof aValue._type === "undefined") && |
|
(typeof aValue._value === "undefined") && |
|
self.hierarchical; |
|
} |
|
|
|
function addItemAndSubItemsToArrayOfAllItems(/* dojo/data/api/Item */ anItem){ |
|
self._arrayOfAllItems.push(anItem); |
|
for(var attribute in anItem){ |
|
var valueForAttribute = anItem[attribute]; |
|
if(valueForAttribute){ |
|
if(lang.isArray(valueForAttribute)){ |
|
var valueArray = valueForAttribute; |
|
for(var k = 0; k < valueArray.length; ++k){ |
|
var singleValue = valueArray[k]; |
|
if(valueIsAnItem(singleValue)){ |
|
addItemAndSubItemsToArrayOfAllItems(singleValue); |
|
} |
|
} |
|
}else{ |
|
if(valueIsAnItem(valueForAttribute)){ |
|
addItemAndSubItemsToArrayOfAllItems(valueForAttribute); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
this._labelAttr = dataObject.label; |
|
|
|
// We need to do some transformations to convert the data structure |
|
// that we read from the file into a format that will be convenient |
|
// to work with in memory. |
|
|
|
// Step 1: Walk through the object hierarchy and build a list of all items |
|
var i, |
|
item; |
|
this._arrayOfAllItems = []; |
|
this._arrayOfTopLevelItems = dataObject.items; |
|
|
|
for(i = 0; i < this._arrayOfTopLevelItems.length; ++i){ |
|
item = this._arrayOfTopLevelItems[i]; |
|
if(lang.isArray(item)){ |
|
addingArrays = true; |
|
} |
|
addItemAndSubItemsToArrayOfAllItems(item); |
|
item[this._rootItemPropName]=true; |
|
} |
|
|
|
// Step 2: Walk through all the attribute values of all the items, |
|
// and replace single values with arrays. For example, we change this: |
|
// { name:'Miss Piggy', pets:'Foo-Foo'} |
|
// into this: |
|
// { name:['Miss Piggy'], pets:['Foo-Foo']} |
|
// |
|
// We also store the attribute names so we can validate our store |
|
// reference and item id special properties for the O(1) isItem |
|
var allAttributeNames = {}, |
|
key; |
|
|
|
for(i = 0; i < this._arrayOfAllItems.length; ++i){ |
|
item = this._arrayOfAllItems[i]; |
|
for(key in item){ |
|
if(key !== this._rootItemPropName){ |
|
var value = item[key]; |
|
if(value !== null){ |
|
if(!lang.isArray(value)){ |
|
item[key] = [value]; |
|
} |
|
}else{ |
|
item[key] = [null]; |
|
} |
|
} |
|
allAttributeNames[key]=key; |
|
} |
|
} |
|
|
|
// Step 3: Build unique property names to use for the _storeRefPropName and _itemNumPropName |
|
// This should go really fast, it will generally never even run the loop. |
|
while(allAttributeNames[this._storeRefPropName]){ |
|
this._storeRefPropName += "_"; |
|
} |
|
while(allAttributeNames[this._itemNumPropName]){ |
|
this._itemNumPropName += "_"; |
|
} |
|
while(allAttributeNames[this._reverseRefMap]){ |
|
this._reverseRefMap += "_"; |
|
} |
|
|
|
// Step 4: Some data files specify an optional 'identifier', which is |
|
// the name of an attribute that holds the identity of each item. |
|
// If this data file specified an identifier attribute, then build a |
|
// hash table of items keyed by the identity of the items. |
|
var arrayOfValues; |
|
|
|
var identifier = dataObject.identifier; |
|
if(identifier){ |
|
this._itemsByIdentity = {}; |
|
this._features['dojo.data.api.Identity'] = identifier; |
|
for(i = 0; i < this._arrayOfAllItems.length; ++i){ |
|
item = this._arrayOfAllItems[i]; |
|
arrayOfValues = item[identifier]; |
|
var identity = arrayOfValues[0]; |
|
if(!Object.hasOwnProperty.call(this._itemsByIdentity, identity)){ |
|
this._itemsByIdentity[identity] = item; |
|
}else{ |
|
if(this._jsonFileUrl){ |
|
throw new Error(this.declaredClass + ": The json data as specified by: [" + this._jsonFileUrl + "] is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]"); |
|
}else if(this._jsonData){ |
|
throw new Error(this.declaredClass + ": The json data provided by the creation arguments is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]"); |
|
} |
|
} |
|
} |
|
}else{ |
|
this._features['dojo.data.api.Identity'] = Number; |
|
} |
|
|
|
// Step 5: Walk through all the items, and set each item's properties |
|
// for _storeRefPropName and _itemNumPropName, so that store.isItem() will return true. |
|
for(i = 0; i < this._arrayOfAllItems.length; ++i){ |
|
item = this._arrayOfAllItems[i]; |
|
item[this._storeRefPropName] = this; |
|
item[this._itemNumPropName] = i; |
|
} |
|
|
|
// Step 6: We walk through all the attribute values of all the items, |
|
// looking for type/value literals and item-references. |
|
// |
|
// We replace item-references with pointers to items. For example, we change: |
|
// { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } |
|
// into this: |
|
// { name:['Kermit'], friends:[miss_piggy] } |
|
// (where miss_piggy is the object representing the 'Miss Piggy' item). |
|
// |
|
// We replace type/value pairs with typed-literals. For example, we change: |
|
// { name:['Nelson Mandela'], born:[{_type:'Date', _value:'1918-07-18'}] } |
|
// into this: |
|
// { name:['Kermit'], born:(new Date(1918, 6, 18)) } |
|
// |
|
// We also generate the associate map for all items for the O(1) isItem function. |
|
for(i = 0; i < this._arrayOfAllItems.length; ++i){ |
|
item = this._arrayOfAllItems[i]; // example: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } |
|
for(key in item){ |
|
arrayOfValues = item[key]; // example: [{_reference:{name:'Miss Piggy'}}] |
|
for(var j = 0; j < arrayOfValues.length; ++j){ |
|
value = arrayOfValues[j]; // example: {_reference:{name:'Miss Piggy'}} |
|
if(value !== null && typeof value == "object"){ |
|
if(("_type" in value) && ("_value" in value)){ |
|
var type = value._type; // examples: 'Date', 'Color', or 'ComplexNumber' |
|
var mappingObj = this._datatypeMap[type]; // examples: Date, dojo.Color, foo.math.ComplexNumber, {type: dojo.Color, deserialize(value){ return new dojo.Color(value)}} |
|
if(!mappingObj){ |
|
throw new Error("dojo.data.ItemFileReadStore: in the typeMap constructor arg, no object class was specified for the datatype '" + type + "'"); |
|
}else if(lang.isFunction(mappingObj)){ |
|
arrayOfValues[j] = new mappingObj(value._value); |
|
}else if(lang.isFunction(mappingObj.deserialize)){ |
|
arrayOfValues[j] = mappingObj.deserialize(value._value); |
|
}else{ |
|
throw new Error("dojo.data.ItemFileReadStore: Value provided in typeMap was neither a constructor, nor a an object with a deserialize function"); |
|
} |
|
} |
|
if(value._reference){ |
|
var referenceDescription = value._reference; // example: {name:'Miss Piggy'} |
|
if(!lang.isObject(referenceDescription)){ |
|
// example: 'Miss Piggy' |
|
// from an item like: { name:['Kermit'], friends:[{_reference:'Miss Piggy'}]} |
|
arrayOfValues[j] = this._getItemByIdentity(referenceDescription); |
|
}else{ |
|
// example: {name:'Miss Piggy'} |
|
// from an item like: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } |
|
for(var k = 0; k < this._arrayOfAllItems.length; ++k){ |
|
var candidateItem = this._arrayOfAllItems[k], |
|
found = true; |
|
for(var refKey in referenceDescription){ |
|
if(candidateItem[refKey] != referenceDescription[refKey]){ |
|
found = false; |
|
} |
|
} |
|
if(found){ |
|
arrayOfValues[j] = candidateItem; |
|
} |
|
} |
|
} |
|
if(this.referenceIntegrity){ |
|
var refItem = arrayOfValues[j]; |
|
if(this.isItem(refItem)){ |
|
this._addReferenceToMap(refItem, item, key); |
|
} |
|
} |
|
}else if(this.isItem(value)){ |
|
//It's a child item (not one referenced through _reference). |
|
//We need to treat this as a referenced item, so it can be cleaned up |
|
//in a write store easily. |
|
if(this.referenceIntegrity){ |
|
this._addReferenceToMap(value, item, key); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
}, |
|
|
|
_addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){ |
|
// summary: |
|
// Method to add an reference map entry for an item and attribute. |
|
// description: |
|
// Method to add an reference map entry for an item and attribute. |
|
// refItem: |
|
// The item that is referenced. |
|
// parentItem: |
|
// The item that holds the new reference to refItem. |
|
// attribute: |
|
// The attribute on parentItem that contains the new reference. |
|
|
|
//Stub function, does nothing. Real processing is in ItemFileWriteStore. |
|
}, |
|
|
|
getIdentity: function(/* dojo/data/api/Item */ item){ |
|
// summary: |
|
// See dojo/data/api/Identity.getIdentity() |
|
var identifier = this._features['dojo.data.api.Identity']; |
|
if(identifier === Number){ |
|
return item[this._itemNumPropName]; // Number |
|
}else{ |
|
var arrayOfValues = item[identifier]; |
|
if(arrayOfValues){ |
|
return arrayOfValues[0]; // Object|String |
|
} |
|
} |
|
return null; // null |
|
}, |
|
|
|
fetchItemByIdentity: function(/* Object */ keywordArgs){ |
|
// summary: |
|
// See dojo/data/api/Identity.fetchItemByIdentity() |
|
|
|
// Hasn't loaded yet, we have to trigger the load. |
|
var item, |
|
scope; |
|
if(!this._loadFinished){ |
|
var self = this; |
|
//Do a check on the JsonFileUrl and crosscheck it. |
|
//If it doesn't match the cross-check, it needs to be updated |
|
//This allows for either url or _jsonFileUrl to he changed to |
|
//reset the store load location. Done this way for backwards |
|
//compatibility. People use _jsonFileUrl (even though officially |
|
//private. |
|
if(this._jsonFileUrl !== this._ccUrl){ |
|
kernel.deprecated(this.declaredClass + ": ", |
|
"To change the url, set the url property of the store," + |
|
" not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0"); |
|
this._ccUrl = this._jsonFileUrl; |
|
this.url = this._jsonFileUrl; |
|
}else if(this.url !== this._ccUrl){ |
|
this._jsonFileUrl = this.url; |
|
this._ccUrl = this.url; |
|
} |
|
|
|
//See if there was any forced reset of data. |
|
if(this.data != null && this._jsonData == null){ |
|
this._jsonData = this.data; |
|
this.data = null; |
|
} |
|
|
|
if(this._jsonFileUrl){ |
|
|
|
if(this._loadInProgress){ |
|
this._queuedFetches.push({args: keywordArgs}); |
|
}else{ |
|
this._loadInProgress = true; |
|
var getArgs = { |
|
url: self._jsonFileUrl, |
|
handleAs: "json-comment-optional", |
|
preventCache: this.urlPreventCache, |
|
failOk: this.failOk |
|
}; |
|
var getHandler = xhr.get(getArgs); |
|
getHandler.addCallback(function(data){ |
|
var scope = keywordArgs.scope?keywordArgs.scope:kernel.global; |
|
try{ |
|
self._getItemsFromLoadedData(data); |
|
self._loadFinished = true; |
|
self._loadInProgress = false; |
|
item = self._getItemByIdentity(keywordArgs.identity); |
|
if(keywordArgs.onItem){ |
|
keywordArgs.onItem.call(scope, item); |
|
} |
|
self._handleQueuedFetches(); |
|
}catch(error){ |
|
self._loadInProgress = false; |
|
if(keywordArgs.onError){ |
|
keywordArgs.onError.call(scope, error); |
|
} |
|
} |
|
}); |
|
getHandler.addErrback(function(error){ |
|
self._loadInProgress = false; |
|
if(keywordArgs.onError){ |
|
var scope = keywordArgs.scope?keywordArgs.scope:kernel.global; |
|
keywordArgs.onError.call(scope, error); |
|
} |
|
}); |
|
} |
|
|
|
}else if(this._jsonData){ |
|
// Passed in data, no need to xhr. |
|
self._getItemsFromLoadedData(self._jsonData); |
|
self._jsonData = null; |
|
self._loadFinished = true; |
|
item = self._getItemByIdentity(keywordArgs.identity); |
|
if(keywordArgs.onItem){ |
|
scope = keywordArgs.scope?keywordArgs.scope:kernel.global; |
|
keywordArgs.onItem.call(scope, item); |
|
} |
|
} |
|
}else{ |
|
// Already loaded. We can just look it up and call back. |
|
item = this._getItemByIdentity(keywordArgs.identity); |
|
if(keywordArgs.onItem){ |
|
scope = keywordArgs.scope?keywordArgs.scope:kernel.global; |
|
keywordArgs.onItem.call(scope, item); |
|
} |
|
} |
|
}, |
|
|
|
_getItemByIdentity: function(/* Object */ identity){ |
|
// summary: |
|
// Internal function to look an item up by its identity map. |
|
var item = null; |
|
if(this._itemsByIdentity){ |
|
// If this map is defined, we need to just try to get it. If it fails |
|
// the item does not exist. |
|
if(Object.hasOwnProperty.call(this._itemsByIdentity, identity)){ |
|
item = this._itemsByIdentity[identity]; |
|
} |
|
}else if (Object.hasOwnProperty.call(this._arrayOfAllItems, identity)){ |
|
item = this._arrayOfAllItems[identity]; |
|
} |
|
if(item === undefined){ |
|
item = null; |
|
} |
|
return item; // Object |
|
}, |
|
|
|
getIdentityAttributes: function(/* dojo/data/api/Item */ item){ |
|
// summary: |
|
// See dojo/data/api/Identity.getIdentityAttributes() |
|
|
|
var identifier = this._features['dojo.data.api.Identity']; |
|
if(identifier === Number){ |
|
// If (identifier === Number) it means getIdentity() just returns |
|
// an integer item-number for each item. The dojo/data/api/Identity |
|
// spec says we need to return null if the identity is not composed |
|
// of attributes |
|
return null; // null |
|
}else{ |
|
return [identifier]; // Array |
|
} |
|
}, |
|
|
|
_forceLoad: function(){ |
|
// summary: |
|
// Internal function to force a load of the store if it hasn't occurred yet. This is required |
|
// for specific functions to work properly. |
|
var self = this; |
|
//Do a check on the JsonFileUrl and crosscheck it. |
|
//If it doesn't match the cross-check, it needs to be updated |
|
//This allows for either url or _jsonFileUrl to he changed to |
|
//reset the store load location. Done this way for backwards |
|
//compatibility. People use _jsonFileUrl (even though officially |
|
//private. |
|
if(this._jsonFileUrl !== this._ccUrl){ |
|
kernel.deprecated(this.declaredClass + ": ", |
|
"To change the url, set the url property of the store," + |
|
" not _jsonFileUrl. _jsonFileUrl support will be removed in 2.0"); |
|
this._ccUrl = this._jsonFileUrl; |
|
this.url = this._jsonFileUrl; |
|
}else if(this.url !== this._ccUrl){ |
|
this._jsonFileUrl = this.url; |
|
this._ccUrl = this.url; |
|
} |
|
|
|
//See if there was any forced reset of data. |
|
if(this.data != null){ |
|
this._jsonData = this.data; |
|
this.data = null; |
|
} |
|
|
|
if(this._jsonFileUrl){ |
|
var getArgs = { |
|
url: this._jsonFileUrl, |
|
handleAs: "json-comment-optional", |
|
preventCache: this.urlPreventCache, |
|
failOk: this.failOk, |
|
sync: true |
|
}; |
|
var getHandler = xhr.get(getArgs); |
|
getHandler.addCallback(function(data){ |
|
try{ |
|
//Check to be sure there wasn't another load going on concurrently |
|
//So we don't clobber data that comes in on it. If there is a load going on |
|
//then do not save this data. It will potentially clobber current data. |
|
//We mainly wanted to sync/wait here. |
|
//TODO: Revisit the loading scheme of this store to improve multi-initial |
|
//request handling. |
|
if(self._loadInProgress !== true && !self._loadFinished){ |
|
self._getItemsFromLoadedData(data); |
|
self._loadFinished = true; |
|
}else if(self._loadInProgress){ |
|
//Okay, we hit an error state we can't recover from. A forced load occurred |
|
//while an async load was occurring. Since we cannot block at this point, the best |
|
//that can be managed is to throw an error. |
|
throw new Error(this.declaredClass + ": Unable to perform a synchronous load, an async load is in progress."); |
|
} |
|
}catch(e){ |
|
console.log(e); |
|
throw e; |
|
} |
|
}); |
|
getHandler.addErrback(function(error){ |
|
throw error; |
|
}); |
|
}else if(this._jsonData){ |
|
self._getItemsFromLoadedData(self._jsonData); |
|
self._jsonData = null; |
|
self._loadFinished = true; |
|
} |
|
} |
|
}); |
|
//Mix in the simple fetch implementation to this class. |
|
lang.extend(ItemFileReadStore,simpleFetch); |
|
|
|
return ItemFileReadStore; |
|
|
|
});
|
|
|