All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.smartclient.debug.public.sc.client.application.Offline.js Maven / Gradle / Ivy

The newest version!
/*
 * Isomorphic SmartClient
 * Version SC_SNAPSHOT-2011-08-08 (2011-08-08)
 * Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
 * "SmartClient" is a trademark of Isomorphic Software, Inc.
 *
 * [email protected]
 *
 * http://smartclient.com/license
 */


// This file encapsulates SmartClient's HTML5 (and fallbacks) offline support.  For ease of 
// porting to pure GWT, isc.Offline is created as a plain JS object with no dependence on 
// the SmartClient class system



(function () {

//> @class Offline
// The Offline class provides features for storing and retrieving values in persistent browser
// storage.  Because this storage is both persistent (ie, it remains available after the 
// browser is closed and re-opened) and local to the browser, it allows applications to operate
// when the client is not connected to a network, albeit with some obvious limitations.
// 

// As well as providing straightforward APIs for storing, retrieving and removing values, // Offline support is integrated into the normal request/response cycle. You can configure // the framework so that server responses are automatically cached in Offline storage. Then, // at some future point when the client is offline, responses for known requests are returned // from the Offline cache. //

// SmartClient makes use of various underlying storage techniques, depending on what the // browser supports, but the API to a SmartClient program is the same regardless of the // underlying storage - simple key/value pairs. Currently, we provide Offline support for all // HTML5-compliant browsers, plus earlier versions of Internet Explorer (6 and 7). The amount // of storage available is dictated by the browser, and varies from approximately 500KB to // approximately 5MB. // // @example offlineSupport // @example offlinePrefs // @treeLocation Client Reference/Data Binding // @group offlineGroup // @visibility external //< var Offline = { // Offline detection and manual override // --------------------------------------------------------------------------------------- explicitOffline: null, //> @classMethod Offline.isOffline() // Returns true if the current browser session is offline (ie, not connected to a network). // If an online/offline state has been set explicitly (see +link{Offline.goOffline()} and // +link{Offline.goOnline}), the explicitly-set state will be returned. Otherwise, the // offline state as reported by the browser will be returned. See // +link{useNativeOfflineDetection,useNativeOfflineDetection} for important notes on // browser detection of offline state. // // @return (boolean) true if the current browser session is offline // @see Offline.goOffline() // @see Offline.goOnline() // @see Offline.useNativeOfflineDetection() // @visibility external //< isOffline : function () { // If the explicit offline flag is set (ie, not null), use it in preference to whatever // the browser reports if (this.explicitOffline !== null) return this.explicitOffline; // Working via a local var so it can be flipped in the debugger for ease of testing var offline = window.navigator.onLine ? false : true; return offline; }, //> @classMethod Offline.goOffline() // Explicitly sets this session into offline mode. This setting will override whatever // state the browser reports. This allows users to manually set an application into // offline or online state. // // @see Offline.goOnline() // @see Offline.useNativeOfflineDetection() // @visibility external //< goOffline : function () { this.explicitOffline = true; }, //> @classMethod Offline.goOnline() // Explicitly sets this session into online mode. This setting will override whatever // state the browser reports. This allows users to manually set an application into // offline or online state. // // @see Offline.goOffline() // @see Offline.useNativeOfflineDetection() // @visibility external //< goOnline : function () { this.explicitOffline = false; }, //> @classMethod Offline.useNativeOfflineDetection() // Tells the Offline system to query the browser for the current online/offline state. // Calling this method switches off the explicit offline mode setting switched on by // calling +link{Offline.goOnline()} or +link{Offline.goOffline()}. //

// It is important to note that browsers vary quite considerably in their ability to // detect that they are offline. Many older browsers simply can't do it; HTML5 browsers // expose the navigator.onLine property, but each browser's implementation // is different. Some browsers have a manual "Work Offline" mode which allows the user // to make the decision, and SmartClient provides an equivalent mechanism with the // goOffline and goOnline methods. Generally speaking, these // methods are more reliable than allowing the browser to decide whether your application // is offline. // // @see Offline.goOnline() // @see Offline.goOffline() // @visibility external //< useNativeOfflineDetection : function () { this.explicitOffline = null; }, // Overall cache managment // --------------------------------------------------------------------------------------- KEY_PREFIX: "isc-", LOCAL_STORAGE: "localStorage", // HTML5 browsers GLOBAL_STORAGE: "globalStorage", // Older versions of Firefox DATABASE_STORAGE: "databaseStorage", // Safari 3.1 & 3.2 (HTML5 browsers as well, // but these older Safari versions have no // localStorage support) GEARS_DATABASE_API: "gears database api", // Built into pre-4 versions of Chrome USERDATA_PERSISTENCE: "userData persistence", // IE5 and later (IE8+ also support localStorage) GOOGLE_GEARS: "google gears", // An explicitly installed Gears instance NO_MECHANISM: "no discovered mechanism", // We won't get this very often! maxResponsesToPersist: 100, userDataPersistenceInIE8: true, localStorageType : function () { if (window.localStorage) { if (!this.userDataPersistenceInIE8 || !isc.Browser.isIE) { return this.LOCAL_STORAGE; } } if (window.globalStorage) return this.GLOBAL_STORAGE; if (window.openDatabase) return this.DATABASE_STORAGE; if (isc.Browser.isIE && isc.Browser.version >= 5) { if (!UserDataPersistence.isInitialized) UserDataPersistence.init(); return this.USERDATA_PERSISTENCE; } // Not implementing anything else just yet return this.NO_MECHANISM; }, // The "native" entry count includes metadata that we store for management and metrics getNativeStoredValuesCount : function () { switch (this.localStorageType()) { case this.LOCAL_STORAGE: return localStorage.length; break; case this.USERDATA_PERSISTENCE: return UserDataPersistence.getNativeStoredValuesCount(); break; case this.GLOBAL_STORAGE: case this.DATABASE_STORAGE: case this.GEARS_DATABASE_API: case this.GOOGLE_GEARS: break; } }, // The "SC" entry count is just data entries, gleaned from our own metrics getSCStoredValuesCount : function () { var entries = this.get(this.countKey); entries = entries ? entries * 1 : 0; return entries; }, // The "native index" is the index into the underlying browser storage; it is not a stable // or predictable value, and thus not a particularly useful one, unless you just want to // do an unordered walk of the cache entries. If you want to iterate over the stored // values in a stable, guaranteed sequence, use getPriorityQueueKey/Value/Entry. This // method was only provided to give autoTests a way of verifying that priority queue // contents are an exact match for actual stored keys getKeyForNativeIndex : function (index) { switch (this.localStorageType()) { case this.LOCAL_STORAGE: return localStorage.key(index); case this.USERDATA_PERSISTENCE: return UserDataPersistence.getKeyForNativeIndex(index); break; case this.GLOBAL_STORAGE: case this.DATABASE_STORAGE: case this.GEARS_DATABASE_API: case this.GOOGLE_GEARS: break; } }, clearCache : function () { var count = this.getSCStoredValuesCount(); while (this.getSCStoredValuesCount() > 0) { this.removeOldestEntry(); // This just prevents an eternal loop if for whatever reason running // removeOldestEntry() does not result in a different count if (this.getSCStoredValuesCount() == count) break; count = this.getSCStoredValuesCount(); } }, clearCacheNatively : function () { // userData is nearly always a special case... if (this.localStorageType() == this.USERDATA_PERSISTENCE) { UserDataPersistence.clearCacheNatively(); return; } var count = this.getNativeStoredValuesCount(); this.logDebug("Removing all " + count + " entries from local storage"); for (var i = 0; i < count; i++) { this._remove(this.getKeyForNativeIndex(0), false); } }, logCacheContents : function (maxEntryLength) { var contents = this.getCacheContents(); this.logDebug("Dumping the contents of the browser's local storage:"); for (var key in contents) { var value = contents[key]; if (value && value.length > maxEntryLength) { value = value.substring(0, maxEntryLength); } this.logDebug(key + " ===> " + value); } }, getCacheContents : function () { var index = 0, contents = {}, count = this.getSCStoredValuesCount(); while (index < count) { var key = this.getPriorityQueueKey(index); // Since we now work via the managed priority queue, we will never get back // anything we disn't insert, so this test will always be true if (key.indexOf(this.KEY_PREFIX) == 0) key = key.substring(this.KEY_PREFIX.length); contents[key] = this.get(key); index++; } return contents; }, getCacheKeys : function () { var index = 0, keys = [], count = this.getSCStoredValuesCount(); while (index < count) { var obj = this.getPriorityQueueKey(index); keys[keys.length] = key; index++; } return keys; }, removeOldestEntry : function () { var key = this.getAndRemoveOldestFromPriorityQueue(); if (key == null) return; this.remove(key, true); }, // Log the raw contents of userData storage - only intended for internal debug use _logUserDataContents : function (maxEntryLength) { var doc = this.userDataSpan.xmlDocument; var maxEntryLength = 100; this.logDebug("Dumping the contents of raw userData storage:"); for (var i = 0; i < this.userDataSpan.xmlDocument.firstChild.attributes.length; i++) { var key = this.userDataSpan.xmlDocument.firstChild.attributes[i].name; var value = this.userDataSpan.getAttribute(key); if (value && value.length > maxEntryLength) { value = value.substring(0, maxEntryLength) + "..."; } this.logDebug(key + " ===> " + value); } }, // Managing entries // --------------------------------------------------------------------------------------- //> @classMethod Offline.put() // Stores the passed-in value in browser-local storage, mapped to the passed-in key. If // there is no room left to store the value, we discard values from the offline store, // oldest first, until there is room to store the value. If you don't want this behavior, // explicitly pass false in the recycleEntries parameter. //

// Note that limitations in the underlying storage engines mean that only primitive // values - Strings, numbers and booleans - can be stored. If you wish to store an Array // or Object, you will have to serialize it to JSON first, and then eval it // after retrieval to turn it back into an object. //

// Note: This method throws an exception if it could not store the value (either // because storage is full and recycleEntries was false, or because the value to store is // simply too large) // // @param key (String) The key to use when storing the value // @param value (any) The value to store // @param [recycleEntries] (boolean) If false, suppresses the default behavior of repeatedly // discarding the oldest entry if there is insufficient // space to store the value // @see Offline.get() // @visibility external //< put : function (key, value, recycleEntries) { var ts = new Date().getTime(); var oldValue = this.get(key); while (true) { try { this._put(key, value); break; } catch (e) { if (recycleEntries !== false && this.isStorageException(e)) { var entries = this.getStorageMetrics().storedEntries; if (entries > 0) { this.logDebug("Cache full; removing oldest entry and trying again"); this.removeOldestEntry(); } else { this.logDebug("Can't add this entry to browser-local storage, even " + "though the cache is empty - the item must be larger " + "than the browser's permitted cache space."); break; } } else { throw e; } } } var end = new Date().getTime(); var pqOK = false, metricsOK = false; while (!pqOK || !metricsOK) { try { if (!pqOK) this.addToPriorityQueue(key); pqOK = true; if (!metricsOK) this.updateMetrics("put", key, value, oldValue); metricsOK = true; } catch(e) { if (this.isStorageException(e)) { if (recycleEntries !== false) { var entries = this.getStorageMetrics().storedEntries; if (entries > 0) { this.logDebug("Cache full when updating priority queue or metrics; " + "removing oldest entry and trying again"); this.removeOldestEntry(); continue; } } this.logDebug("Cache full when updating priority queue or metrics; rolling " + "back the entire update"); this._remove(key); if (pqOK) this.removeFromPriorityQueue(key); this.rebuildMetrics(); throw e; } else { throw e; } } } this.logDebug("put() with key: " + key + "\nitem: " + this.echoLeaf(value) + ": " + (end - ts) + "ms. Maintaining the priority queue and metrics took " + "a further " + new Date().getTime() - end + "ms"); }, _put : function (key, value, applyPrefix) { key = (applyPrefix === false ? "" : this.KEY_PREFIX) + key; switch (this.localStorageType()) { case this.LOCAL_STORAGE: localStorage.setItem(key, value); break; case this.USERDATA_PERSISTENCE: UserDataPersistence.putValue(key, value); break; case this.GLOBAL_STORAGE: // globalStorage[''][key] = value; // break; case this.DATABASE_STORAGE: // this.database.executeSql( // "INSERT INTO ResponseCache (key, value) " + // "('" + key + "', '" + value + "')"); // break; case this.GEARS_DATABASE_API: case this.GOOGLE_GEARS: this.logError("Persistence method '" + this.localStorageType() + "' not yet supported"); break; } }, isStorageException : function (e) { switch (this.localStorageType()) { case this.LOCAL_STORAGE: if (isc.Browser.isIE) { return (e.number == -2147024882); } else if (isc.Browser.isMoz) { return (e.name == "NS_ERROR_DOM_QUOTA_REACHED"); } else { return (e.name == "QUOTA_EXCEEDED_ERR"); } break; case this.USERDATA_PERSISTENCE: return (e.number == -2147024857) } }, //> @classMethod Offline.get() // Returns the value mapped to the passed-in key from browser-local storage, or null if // no such key exists. // // @param key (String) The key to retrieve a value for // @return The value mapped to the passed in key, or null if no such key exists // @see Offline.put() // @visibility external //< get : function (key) { var ts = new Date().getTime(), item; switch (this.localStorageType()) { case this.LOCAL_STORAGE: item = localStorage.getItem(this.KEY_PREFIX + key); break; case this.USERDATA_PERSISTENCE: item = UserDataPersistence.getValue(this.KEY_PREFIX + key); break; case this.GLOBAL_STORAGE: // return globalStorage[''][key]; case this.DATABASE_STORAGE: // Not sure what's involved yet case this.GEARS_DATABASE_API: case this.GOOGLE_GEARS: break; } if (item) item = isc.clone(item); var end = new Date().getTime(); this.logDebug("get() with key: " + key + "\nitem is: " + this.echoLeaf(item) + ": " + (end - ts) + "ms"); return item; }, //> @classMethod Offline.remove() // Removes the key/value pair mapped by the passed-in key from browser-local storage // // @param key (String) The key to remove // @see Offline.put() // @see Offline.get() // @visibility external //< remove : function (key, skipPriorityQueueUpdate) { this.logDebug("Removing item for key: " + key); this.updateMetrics("remove", key); if (!skipPriorityQueueUpdate) this.removeFromPriorityQueue(key); this._remove(key); }, _remove : function (key, applyPrefix) { key = (applyPrefix === false ? "" : this.KEY_PREFIX) + key; switch (this.localStorageType()) { case this.LOCAL_STORAGE: localStorage.removeItem(key); break; case this.USERDATA_PERSISTENCE: UserDataPersistence.removeValue(key); break; case this.GLOBAL_STORAGE: // return globalStorage[''][key]; case this.DATABASE_STORAGE: // Not sure what's involved yet case this.GEARS_DATABASE_API: case this.GOOGLE_GEARS: break; } }, getUndecoratedKey : function (key) { if (key && key.startsWith(this.KEY_PREFIX)) { key = key.substring(this.KEY_PREFIX.length); } return key }, // --------------------------------------------------------------------------------------- priorityQueueKey: "pq", // this will actually have "isc-" prepended to it addToPriorityQueue : function (userKey) { //!DONTOBFUSCATE this.removeFromPriorityQueue(userKey); var key = this.toInternalKey(userKey); var pqText = this.get(this.priorityQueueKey); if (pqText) { eval("var pq = " + pqText); } else { var pq = []; } pq.push(key); this._put(this.priorityQueueKey, this.serialize(pq)); }, removeFromPriorityQueue : function (userKey) { //!DONTOBFUSCATE var key = this.toInternalKey(userKey); var pqText = this.get(this.priorityQueueKey); if (pqText) { eval("var pq = " + pqText); } else { var pq = []; } for (var i = 0; i < pq.length; i++) { if (pq[i] == key) { var leading = pq.slice(0, i); var trailing = pq.slice(i + 1); pq = leading.concat(trailing); break; } } this._put(this.priorityQueueKey, this.serialize(pq)); }, getAndRemoveOldestFromPriorityQueue : function () { //!DONTOBFUSCATE var pqText = this.get(this.priorityQueueKey); if (pqText) { eval("var pq = " + pqText); } else { var pq = []; } var oldest = pq.shift(); this._put(this.priorityQueueKey, this.serialize(pq)); return this.toUserKey(oldest); }, getPriorityQueueEntry : function (index) { var key = this.getPriorityQueueKey(index); var value = this.get(key); var entry = {}; entry[key] = value; return entry; }, getPriorityQueueValue : function (index) { var key = this.getPriorityQueueKey(index); return this.get(key); }, getPriorityQueueKey : function (index) { //!DONTOBFUSCATE var pqText = this.get(this.priorityQueueKey); if (pqText) { eval("var pq = " + pqText); } else { var pq = []; } return this.toUserKey(pq[index]); }, getPriorityQueue : function () { //!DONTOBFUSCATE var pqText = this.get(this.priorityQueueKey); if (pqText) { eval("var pq = " + pqText); } else { var pq = []; } return pq; }, toInternalKey : function (userKey) { if (this.localStorageType() == this.USERDATA_PERSISTENCE) { return UserDataPersistence.getDataStoreKey(this.KEY_PREFIX + userKey); } // Otherwise, we don't mess about with converting keys return userKey; }, toUserKey : function (internalKey) { if (this.localStorageType() == this.USERDATA_PERSISTENCE) { return this.getUndecoratedKey(UserDataPersistence.getUserKey(internalKey)); } // Otherwise, we don't mess about with converting keys return internalKey; }, // Storage metrics // --------------------------------------------------------------------------------------- countKey: "storedEntryCount__", keyKey: "storedKeyBytes__", valueKey: "storedValueBytes__", updateMetrics : function (mode, key, value, oldValue) { var realKey = this.KEY_PREFIX + key, storedEntries = this.get(this.countKey) || 0, storedKeyBytes = this.get(this.keyKey) || 0, storedValueBytes = this.get(this.valueKey) || 0; storedKeyBytes = 1 * storedKeyBytes; storedValueBytes = 1 * storedValueBytes; if (mode == "remove") { var item = this.get(key); storedEntries--; storedKeyBytes -= realKey.length; storedValueBytes -= item.length; } else { if (oldValue == null) { storedEntries++; storedKeyBytes += realKey.length; storedValueBytes += value.length; } else { storedValueBytes += value.length - oldValue.length; } } this._put(this.countKey, storedEntries); this._put(this.keyKey, storedKeyBytes); this._put(this.valueKey, storedValueBytes); }, rebuildMetrics : function () { var pq = this.getPriorityQueue(), entries = 0, keyBytes = 0, valueBytes = 0; for (var i = 0; i < pq.length; i++) { var value = this.get(pq[i]); entries++; keyBytes += pq[i].length; valueBytes += value.length; } this._put(this.countKey, entries); this._put(this.keyKey, keyBytes); this._put(this.valueKey, valueBytes); }, getStorageMetrics : function () { var storedEntries = this.get(this.countKey) || 0, storedKeyBytes = this.get(this.keyKey) || 0, storedValueBytes = this.get(this.valueKey) || 0, countLen = 0, keyLen = 0, valueLen = 0; if (storedEntries) countLen = storedEntries.length; if (storedKeyBytes) keyLen = storedKeyBytes.length; if (storedValueBytes) valueLen = storedValueBytes.length; storedEntries = 1 * storedEntries; storedKeyBytes = 1 * storedKeyBytes; storedValueBytes = 1 * storedValueBytes; var pqText = this.get(this.priorityQueueKey); var overhead = this.countKey.length + this.keyKey.length + this.valueKey.length + countLen + keyLen + valueLen; var pqLength = pqText == null ? 0 : pqText.length + (this.KEY_PREFIX + this.priorityQueueKey).length; return { storedEntries: storedEntries, storedKeyBytes: storedKeyBytes, storedValueBytes: storedValueBytes, metricsOverhead: overhead, priorityQueue: pqLength, total: storedKeyBytes + storedValueBytes + overhead + pqLength } }, getTotalStorageUsed : function () { var metrics = this.getStorageMetrics(); return metrics.storedKeyBytes + metrics.storedValueBytes + metrics.metricsOverhead + metrics.priorityQueue; }, // DataSource functionality // --------------------------------------------------------------------------------------- storeResponse : function (dsRequest, dsResponse) { var ts = new Date().getTime(); dsResponse.offlineTimestamp = ts; var trimmedRequest = this.trimRequest(dsRequest), key = this.serialize(trimmedRequest), value = this.serialize(this.trimResponse(dsResponse)); this.logDebug("storeResponse serializing: " + (new Date().getTime() - ts) + "ms"); // Unless we are already storing a response for this request (in which case we'll just // freshen it), check if we're about to bust the maximum responses limit if (this.get(key) == null) { if (this.getSCStoredValuesCount() >= this.maxResponsesToPersist) { this.removeOldestEntry(); } } this.put(key, value); }, trimRequest : function (dsRequest) { var keyProps = ["dataSource", "operationType", "operationId", "textMatchStyle", "values", "sortBy", "startRow", "endRow", "data"], trimmed = {}, undef; for (var i = 0; i < keyProps.length; i++) { if (dsRequest[keyProps[i]] !== undef) { trimmed[keyProps[i]] = dsRequest[keyProps[i]]; } } return trimmed; }, trimResponse : function (dsResponse) { var keyProps = ["dataSource", "startRow", "endRow", "totalRows", "data", "offlineTimestamp", "status", "errors", // note we don't actually cache errors yet "invalidateCache", "cacheTimestamp"], trimmed = {}, undef; for (var i = 0; i < keyProps.length; i++) { if (dsResponse[keyProps[i]] !== undef) { trimmed[keyProps[i]] = dsResponse[keyProps[i]]; } } return trimmed; }, getResponse : function (dsRequest) { //!DONTOBFUSCATE var trimmedRequest = this.trimRequest(dsRequest), key = this.serialize(trimmedRequest), value = this.get(key), returnValue; // using raw eval() because SmartClient might not be available eval('returnValue = ' + value); if (returnValue) returnValue.fromOfflineCache = true; return returnValue; }, serialize : function (obj) { return isc.Comm.serialize(obj, false); }, // Offline storage browser // --------------------------------------------------------------------------------------- showStorageInfo : function () { if (!this.storageBrowser) { if (isc.Offline.localStorageType() == isc.Offline.USERDATA_PERSISTENCE) { isc.Timer.setTimeout(function () { isc.say("WARNING: This browser uses an old storage mechanism that does not " + "permit arbitrary key/value pair storage. This means we have to " + "store extra management data, with the upshot that the metrics reported " + "for 'priority queue' and 'overhead' are indicative, but not accurate"); }, 0); } this.metricsDF = isc.DynamicForm.create({ width: "100%", numCols: 6, fields: [ {name: "storedEntries", title: "No. entries", disabled: true}, {name: "storedKeyBytes", title: "Used by keys", disabled: true}, {name: "storedValueBytes", title: "Used by values", disabled: true}, {name: "priorityQueue", title: "Used by Priority Queue", disabled: true}, {name: "metricsOverhead", title: "Metrics overhead", disabled: true}, {name: "total", title: "Total Bytes", disabled: true} ] }); this.storageLG = isc.ListGrid.create({ width: "100%", height: "*", canRemoveRecords: true, removeData: function (record) { isc.ask("Remove this entry?", function (value) { if (value) { isc.Offline.remove(record.key); isc.Offline.refreshStorageInfo(); } }); }, rowDoubleClick: function(record) { isc.Offline.createStorageEditorWindow(); isc.Offline.storageEditorWindow.show(); isc.Offline.storageEditor.editRecord(record); }, fields: [ {name: "key", width: "25%", title: "Key"}, {name: "value", title: "Value"} ] }); this.storageBrowser = isc.Window.create({ autoCenter: true, canDragResize: true, width: Math.floor(isc.Page.getWidth() * 0.5), height: Math.floor(isc.Page.getHeight() * 0.5), title: "Offline Storage", items: [ this.metricsDF, this.storageLG, isc.HLayout.create({ width: "100%", height: 1, members: [ isc.LayoutSpacer.create({width: "*"}), isc.Button.create({ title: "Add Entry", click: function () { isc.Offline.createStorageEditorWindow(); isc.Offline.storageEditorWindow.show(); isc.Offline.storageEditor.editNewRecord(); } }) ] }) ] }); } this.storageBrowser.show(); this.refreshStorageInfo(); }, createStorageEditorWindow : function () { if (!isc.Offline.storageEditorWindow) { isc.Offline.storageEditor = isc.DynamicForm.create({ fields: [ {name: "key", title: "Key", editorType: "TextAreaItem", width: 400}, {name: "value", title: "Value", editorType: "TextAreaItem", width: 400}, {name: "saveButton", type: "button", title: "Save", click: function () { var form = isc.Offline.storageEditor; if (form.saveOperationType == "update" && form.getValue("key") != form.getOldValue("key")) { isc.ask("Key has changed - this will create a new entry. " + "Do you want to retain the old entry as well? (if " + "you answer 'No', it will be removed", function (value) { if (value === false) { isc.Offline.remove(form.getOldValue("key")); } if (value != null) { isc.Offline.put(form.getValue("key"), form.getValue("value")); isc.Offline.storageEditorWindow.hide(); isc.Offline.refreshStorageInfo(); } }); } else { isc.Offline.put(form.getValue("key"), form.getValue("value")); isc.Offline.storageEditorWindow.hide(); isc.Offline.refreshStorageInfo(); } }} ] }); isc.Offline.storageEditorWindow = isc.Window.create({ bodyProperties: { margin: 5 }, title: "Edit Offline Storage Entry", isModal: true, autoCenter: true, height: 280, width: 480, items: [ isc.Offline.storageEditor ] }); } }, refreshStorageInfo : function () { this.metricsDF.editRecord(isc.Offline.getStorageMetrics()); var dataObj = isc.Offline.getCacheContents(); var data = []; for (var key in dataObj) { data.add({key: key, value: dataObj[key]}); } this.storageLG.setData(data); } }; var UserDataPersistence = { isInitialized: false, poolSize: 10, keyIndexKey: "keyIndex", reverseKeyIndexKey: "reverseKeyIndex", init : function () { this.userDataSpan = []; for (var i = 0; i < this.poolSize; i++) { this.userDataSpan[i] = document.createElement('span'); this.userDataSpan[i].ID = 'isc_userData_' + i; this.userDataSpan[i].style.behavior = 'url(#default#userdata)'; document.body.appendChild(this.userDataSpan[i]); this.userDataSpan[i].load("isc_userData_" + i); } this.keyIndexStore = document.createElement('span'); this.keyIndexStore.ID = 'isc_userData_keyIndex'; this.keyIndexStore.style.behavior = 'url(#default#userdata)'; document.body.appendChild(this.keyIndexStore); this.keyIndexStore.load("isc_userData_keyIndex"); this.keyIndex = this.getKeyIndexFromStore(); this.reverseKeyIndex = this.getReverseKeyIndexFromStore(); if (!this.keyIndex) { this.keyIndex = {}; this.reverseKeyIndex = {}; } else if (!this.reverseKeyIndex) this.buildReverseKeyIndex(); this.buildNextAttributeInfo(); this.isInitialized = true; }, clearCacheNatively : function () { for (var i = 0; i < this.poolSize; i++) { var attrs = this.userDataSpan[i].xmlDocument.firstChild.attributes; while (attrs.length > 0) { this.userDataSpan[i].removeAttribute(attrs[0].name); } } this.keyIndex = {}; this.reverseKeyIndex = {}; this.saveKeyIndex(); }, getNativeStoredValuesCount : function () { var count = 0; for (var i = 0; i < this.poolSize; i++) { count += this.userDataSpan[i].xmlDocument.firstChild.attributes.length; } return count; }, getKeyForNativeIndex : function (index) { var iCounter = 0; for (var i = 0; i < this.poolSize; i++) { if (iCounter + this.userDataSpan[i].xmlDocument.firstChild.attributes.length > index) { var offsetIndex = index - iCounter; var attrName = this.userDataSpan[i].xmlDocument.firstChild.attributes[offsetIndex].name, attrNum = attrName.substring(1), dsKey = this.getKeyIndexValue(i, attrNum); return this.getUserKey(dsKey); } } }, getKeyIndexValue : function (index, attrName) { var attrNum = attrName.substring(1); if (index == 0) { return "00000".substring(attrNum.length) + attrNum; } return index * 10000 + (1 * attrNum); }, getUserKey : function (userKey) { return this.reverseKeyIndex[userKey]; }, getDataStoreKey : function (key) { return this.keyIndex[key]; }, _getValue : function (dataStore, attr) { return this.userDataSpan[dataStore].getAttribute(attr); }, getValue : function (userKey) { var key = this.getDataStoreKey(userKey), undef; if (key === undef) return null; var dataStore = ("" + key).substring(0, 1), attr = "v" + (("" + key).substring(1) * 1); return this._getValue(dataStore, attr); }, putValue : function (userKey, value) { var key = this.getDataStoreKey(userKey); if (key) { // We're already storing a value for this key var dataStore = ("" + key).substring(0, 1), attr = "v" + (("" + key).substring(1) * 1), savedValue = this._getValue(dataStore, attr); } else { var dataStore = this.getDataStoreForNewItem(), attr = this.getNextAttributeName(dataStore); } this.userDataSpan[dataStore].setAttribute(attr, value); try { this.userDataSpan[dataStore].save("isc_userData_" + dataStore); this.addToKeyIndex(userKey, dataStore, attr); } catch(e) { if (isc.Offline.isStorageException(e)) { if (savedValue) { this.userDataSpan[dataStore].setAttribute(attr, savedValue); } else { this.userDataSpan[dataStore].removeAttribute(attr); this.removeFromKeyIndex(userKey); } } throw e; } }, removeValue : function (userKey) { var key = this.getDataStoreKey(userKey), undef; if (key === undef) { Offline.logDebug("userData: in removeValue, no value for key '" + userKey + "' was found"); return; } var dataStore = ("" + key).substring(0, 1), attr = "v" + (("" + key).substring(1) * 1); this.userDataSpan[dataStore].removeAttribute(attr); this.userDataSpan[dataStore].save("isc_userData_" + dataStore); this.removeFromKeyIndex(userKey); this.unusedAttributeNumbers[dataStore].push(attr.substring(1) * 1); }, getDataStoreForNewItem : function () { // TODO: For now, we'll allocate items to stores on a simple round-robin basis; always // allocating to the least-full store is a better approach, but requires us to track // additional metrics var undef; if (this.nextDataStoreToUse === undef) { // Select the start point randomly, to avoid overusing the the first few pools in // applications that are restarted often, relative to the number of items they store this.nextDataStoreToUse = Math.floor(Math.random() * this.poolSize); } var rtnValue = this.nextDataStoreToUse++; if (this.nextDataStoreToUse >= this.poolSize) this.nextDataStoreToUse = 0; return rtnValue; }, buildNextAttributeInfo : function () { this.nextAttributeNumber = []; this.unusedAttributeNumbers = []; for (var i = 0; i < this.poolSize; i++) { this.unusedAttributeNumbers[i] = []; var attrs = this.userDataSpan[i].xmlDocument.firstChild.attributes; var work = []; for (var j = 0; j < attrs.length; j++) { var num = attrs[j].name.substring(1) * 1; if (!isNaN(num)) work.add(attrs[j].name.substring(1) * 1); } if (work.sort) work.sort(); else this.sort(work); var counter = 0; for (j = 0; j < work.length; j++) { if (work[j] == counter) { counter++; continue; } while(work[j] != counter && counter <= 9999) { this.unusedAttributeNumbers[i].push(counter++); } counter++; } this.nextAttributeNumber[i] = counter; } }, //>GWT_Standalone sort : function (array) { for (var i = 0; i < array.length; i++) { var swapped = false; for (var j = 1; j < array.length - i; j++) { if (array[j] < array[j-1]) { var temp = array[j]; array[j] = array[j-1]; array[j-1] = temp; swapped = true; } } if (!swapped) break; } }, // 0) { return "v" + this.unusedAttributeNumbers[dataStore].shift(); } if (this.nextAttributeNumber[dataStore] == null) { this.nextAttributeNumber[dataStore] = 1; } return "v" + this.nextAttributeNumber[dataStore]++; }, addToKeyIndex : function (userKey, dataStore, attr) { var keyIndexValue = this.getKeyIndexValue(dataStore, attr); this.keyIndex[userKey] = keyIndexValue; this.reverseKeyIndex[keyIndexValue] = userKey; this.saveKeyIndex(); }, removeFromKeyIndex : function (userKey) { var keyIndexValue = this.keyIndex[userKey]; delete this.keyIndex[userKey]; delete this.reverseKeyIndex[keyIndexValue]; this.saveKeyIndex(); }, saveKeyIndex : function () { this.keyIndexStore.setAttribute(this.keyIndexKey, Offline.serialize(this.keyIndex)); this.keyIndexStore.setAttribute(this.reverseKeyIndexKey, Offline.serialize(this.reverseKeyIndex)); this.keyIndexStore.save("isc_userData_keyIndex"); }, buildReverseKeyIndex : function() { this.reverseKeyIndex = {}; for (var key in this.keyIndex) { this.reverseKeyIndex[keyIndex[key]] = key; } }, getKeyIndexFromStore : function () { //!DONTOBFUSCATE var kiText = this.keyIndexStore.getAttribute(this.keyIndexKey); if (kiText) { eval("var ki = " + kiText); } else { var ki = null; } return ki; }, getReverseKeyIndexFromStore : function () { //!DONTOBFUSCATE var kiText = this.keyIndexStore.getAttribute(this.reverseKeyIndexKey); if (kiText) { eval("var ki = " + kiText); } else { var ki = null; } return ki; } }; if (window.isc) { isc.defineClass("Offline").addClassProperties(Offline); isc.defineClass("UserDataPersistence").addClassProperties(UserDataPersistence); } else { //>GWT_Standalone isc.addProperties = function (objOne, objTwo) { for (var propName in objTwo) objOne[propName] = objTwo[propName]; } isc.addProperties(isc.Offline, { // utilities // --------------------------------------------------------------------------------------- serialize : function (object) { return isc.OfflineJSONEncoder.encode(object); }, logDebug : function (message) { if (console) console.log(message); }, logError : function (message) { if (console) { console.log(message); } else { alert(message); } }, echoLeaf : function (obj) { var output = "", undefined; if (obj === undefined) return "undef"; try { if (typeof obj == "Array") { output += "Array[" + obj.length + "]"; } else if (typeof obj == "Date") { output += "Date(" + obj.toShortDate() + ")"; } else if (typeof obj == "Function") { output += isc.Func.getName(obj, true) + "()"; } else { switch (typeof obj) { case "string" : // for shorter strings show the whole thing. Also, in "longMode" don't // shorten. if (obj.length <= 40) { output += '"' + obj + '"'; break; } // for long strings, show an elipsis and the strings full length output += '"' + obj.substring(0, 40) + '..."[' + obj.length + ']'; // convert CR/LF to avoid spanning several lines output = output.replaceAll("\n", "\\n").replaceAll("\r", "\\r"); break; case "object" : // typeof null is "object" if (obj == null) { output += "null"; break; } // DOM object if (obj.tagName != null) { output += "[" + obj.tagName + "Element]"; break; } var toString = "" + obj; if (toString != "" && toString != "[object Object]" && toString != "[object]") { // someone went through the trouble of making a better toString(), so // use it. NOTE: check for "" because in IE an XmlNodeList among // others will toString() to "" output += toString; break; } // return generic "Obj" output += "Obj"; break; default: output += "" + obj; // invoke native toString() } } return output; } catch (e) { var message = "[Error in echoLeaf: " + e + "]"; output += message; return output; } }, echo : function (object) { return this.serialize(object); } }); isc.OfflineJSONEncoder = { _serialize_remember : function (objRefs, object, path) { objRefs.obj.add(object); objRefs.path.add(path); }, _serialize_cleanNode : function (object) { var treeId = object["_isc_tree"]; if (treeId != null) { var theTree = window[treeId]; if (theTree && theTree.parentProperty && object[theTree.parentProperty]) { object = theTree.getCleanNodeData(object); } } return object; }, _serialize_alreadyReferenced : function (objRefs, object) { var rowNum = objRefs.obj.indexOf(object); if (rowNum == -1) return null; return objRefs.path[rowNum]; }, _serialize_addToPath : function (objPath, newIdentifier) { if (isc.isA.Number(newIdentifier)) { return objPath + "[" + newIdentifier + "]"; } else if (! isc.Comm._simpleIdentifierRE.test(newIdentifier)) { return objPath + '["' + newIdentifier + '"]'; } else { return objPath + "." + newIdentifier; } }, encode : function (object) { this.objRefs = {obj:[],path:[]}; var retVal = this._serialize(object, this.prettyPrint ? "" : null , null); this.objRefs = null; return retVal }, dateFormat: "xmlSchema", encodeDate : function (date) { if (this.dateFormat == "dateConstructor") { return "new Date(" + date.getTime() + ")"; } else { // quotes for xml schema return '"' + this.toSchemaDate(date) + '"'; } }, toSchemaDate : function (date) { var dd = "" + date.getDate(), mm = "" + (date.getMonth() + 1), yyyy = "" + (date.getyear() + 1900), hh = "" + date.getHours(), mi = "" + date.getMinutes(), ss = "" + date.getSeconds(); dd = dd.length == 1 ? "0" + dd : dd; mm = mm.length == 1 ? "0" + mm : mm; hh = hh.length == 1 ? "0" + hh : ff; mi = mi.length == 1 ? "0" + mi : mi; ss = ss.length == 1 ? "0" + ss : ss; return yyyy + "-" + mm + "-" + dd + "T" + hh + ":" + mi + ":" + ss; }, strictQuoting: true, circularReferenceMode: "path", circularReferenceMarker: "$$BACKREF$$", prettyPrint: false, _serialize : function (object, prefix, objPath) { if (!objPath) { if (object && object.getID) objPath = object.getID(); else objPath = ""; } if (object == null) return null; // handle simple types // In Safari a cross-frame scripting bug means that the 'asSource' method may not always be // available as an instance method. // call the static version of the same method if this happens. if (this.isAString(object)) return this.asSource(object); if (this.isAFunction(object)) return null; if (this.isANumber(object) || this.isASpecialNumber(object)) return object; if (this.isABoolean(object)) return object; if (this.isADate(object)) return this.encodeDate(object); // skip instances (this is a simplification of the root code) if (this.isAnInstance(object)) return null; var prevPath = this._serialize_alreadyReferenced(this.objRefs, object); if (prevPath != null && objPath.contains(prevPath)) { var nextChar = objPath.substring(prevPath.length, prevPath.length+1); if (nextChar == "." || nextChar == "[" || nextChar == "]") { var mode = this.circularReferenceMode; if (mode == "marker") { return "'" + this.circularReferenceMarker + "'"; } else if (mode == "path") { return "'" + this.circularReferenceMarker + ":" + prevPath + "'"; } else { return null; } } } if (this.isAClassObject(object)) return null; if (object == window) return null; this._serialize_remember(this.objRefs, object, objPath); // if there is a serialize method associated with this object, call that // (NOTE: Leaving this in, in case we're running in a SmartClinet context) if (this.isAFunction(object._serialize)) { return object._serialize(prefix, this.objRefs, objPath); } // handle arrays as a special case if (this.isAnArray(object)) { return this._serializeArray(object, objPath, this.objRefs, prefix); } var data; // if the object has a getSerializeableFields, use whatever it returns, otherwise just use the object if (object.getSerializeableFields) { data = object.getSerializeableFields([], []); } else { data = object; } // and return anything else as a simple object return this._serializeObject(data, objPath, this.objRefs, prefix); }, _serializeArray : function (object, objPath, objRefs, prefix) { // Replaced references to the SmartClient StringBuffer class with native strings throughout // this method, rather than port that class's functionality. Also removed pretty print // facilities - we don't need them for this var output = "["; // for each element in the array for (var i = 0, len = object.length; i < len; i++) { var value = object[i]; if (prefix != null) output += "\r" + prefix; var objPath = this._serialize_addToPath(objPath, i); var serializedValue = this._serialize(value, (prefix != null ? prefix : null), objPath); output += serializedValue + ","; if (prefix != null) output += " "; } // get rid of the trailing comma, if any var commaChar = output.lastIndexOf(","); if (commaChar > -1) output = output.substring(0, commaChar); // add the end array marker if (prefix != null) output += "\r" + prefix; output += "]"; // and return the output return output; }, _serializeObject : function (object, objPath, objRefs, prefix) { // Replaced references to the SmartClient StringBuffer class with native strings throughout // this method, rather than port that class's functionality. Also removed pretty print // facilities - we don't need them for this var output = "{", undef; object = this._serialize_cleanNode(object); // Not trying to support every edge case at this point - skipping code that copes with // XMLNodes here, rather than porting isc.echoLeaf try { for (var key in object) break; } catch (e) { return null; } for (var key in object) { if (key == null) continue; if (key == "xmlHttpRequest") continue; // skip internal properties, if the flag is set (simplified this code to remove // framework calls) if (key.substring(0,1) == "_" || key.substring(0,1) == "$") continue; var value = object[key]; if (this.isAFunction(value)) continue; // omit instances entirely if so configured (this code is simplified, we always // skip instances) if (this.isAnInstance(value)) continue; // convert the key to a string var keyStr = key.toString(); // and quote it keyStr = '"' + keyStr + '"'; var objPath = this._serialize_addToPath(objPath, key); var serializedValue; // don't try to serialize references to GWT Java objects if (key != "__ref") { serializedValue = this._serialize(value, (prefix != null ? prefix : null), objPath); } // now output the key : value pair if (prefix != null) output += "\r" + prefix; output += keyStr + ":" + serializedValue + ","; if (prefix != null) output.append(" "); } // get rid of the trailing comma, if any var commaChar = output.lastIndexOf(","); if (commaChar > -1) output = output.substring(0, commaChar); // add the end object marker if (prefix != null) output += "\r" + prefix; output += "}"; // and return the output return output; }, // These methods ripped off from SmartClient's IsA class isAString : function (object) { if (object == null) return false; if (typeof object == this._$function) return false; if (object.constructor && object.constructor.__nativeType != null) { return object.constructor.__nativeType == 4; } if (object.Class != null && object.Class == this._$String) return true; return typeof object == "string"; }, isAnArray : function (object) { if (object == null) return false; if (typeof object == this._$function) return false; if (object.constructor && object.constructor.__nativeType != null) { return object.constructor.__nativeType == 2; } if (isc.Browser.isSafari) return ""+object.splice == "(Internal function)"; return ""+object.constructor == ""+Array; }, isAFunction : function (object) { if (object == null) return false; if (isc.Browser.isIE && typeof object == this._$function) return true; var cons = object.constructor; if (cons && cons.__nativeType != null) { if (cons.__nativeType != 1) return false; if (cons === Function) return true; } return isc.Browser.isIE ? (isc.emptyString+object.constructor == Function.toString()) : (typeof object == this._$function); }, isANumber : function (object) { if (object == null) return false; if (object.constructor && object.constructor.__nativeType != null) { if (object.constructor.__nativeType != 5) return false; } else { if (typeof object != "number") return false; } // it's a number, now check if it's a valid number return !isNaN(object) && object != Number.POSITIVE_INFINITY && object != Number.NEGATIVE_INFINITY; }, isASpecialNumber : function (object) { if (object == null) return false; if (object.constructor && object.constructor.__nativeType != null) { if (object.constructor.__nativeType != 5) return false; } else { if (typeof object != "number") return false; } return (isNaN(object) || object == Number.POSITIVE_INFINITY || object == Number.NEGATIVE_INFINITY); }, isABoolean : function (object) { if (object == null) return false; if (object.constructor && object.constructor.__nativeType != null) { return object.constructor.__nativeType == 6; } return typeof object == "boolean"; }, isADate : function (object) { if (object == null) return false; if (object.constructor && object.constructor.__nativeType != null) { return object.constructor.__nativeType == 3; } return (""+object.constructor) == (""+Date) && object.getDate && isc.isA.Number(object.getDate()) }, isAnXMLNode : function (object) { if (object == null) return false; if (isc.Browser.isIE) { return object.specified != null && object.parsed != null && object.nodeType != null && object.hasChildNodes != null; } var doc = object.ownerDocument; if (doc == null) return false; return doc.contentType == this._$textXML; }, isAnInstance : function (object) { return (object != null && object._scPrototype != null) }, isAClassObject : function (object) { return (object != null && object._isClassObject == true) }, // This method ripped out of the SmartClient String enhancements asSource : function (string, singleQuote) { if (!this.isAString(string)) string = ""+string; var quoteRegex = singleQuote ? String._singleQuoteRegex : String._doubleQuoteRegex, outerQuote = singleQuote ? "'" : '"'; return outerQuote + string.replace(/\\/g, "\\\\") // quote whichever quote we use on the outside .replace(quoteRegex, '\\' + outerQuote) .replace(/\t/g, "\\t") .replace(/\r/g, "\\r") .replace(/\n/g, "\\n") + outerQuote; } }; // close offline serializer definition //





© 2015 - 2024 Weber Informatics LLC | Privacy Policy