1 /** 2 * The Render Engine 3 * BrowserStorage 4 * 5 * @fileoverview Generalized browser-based storage class for W3C storage types. 6 * 7 * @author: Brett Fattori (brettf@renderengine.com) 8 * @author: $Author: bfattori@gmail.com $ 9 * @version: $Revision: 1557 $ 10 * 11 * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com) 12 * 13 * Permission is hereby granted, free of charge, to any person obtaining a copy 14 * of this software and associated documentation files (the "Software"), to deal 15 * in the Software without restriction, including without limitation the rights 16 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 * copies of the Software, and to permit persons to whom the Software is 18 * furnished to do so, subject to the following conditions: 19 * 20 * The above copyright notice and this permission notice shall be included in 21 * all copies or substantial portions of the Software. 22 * 23 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 * THE SOFTWARE. 30 * 31 */ 32 33 // The class this file defines and its required classes 34 R.Engine.define({ 35 "class":"R.storage.BrowserStorage", 36 "requires":[ 37 "R.storage.AbstractDBStorage", 38 "R.util.FNV1Hash" 39 ], 40 "includes":[ 41 "/libs/trimpath-query-1.1.14.js" 42 ] 43 }); 44 45 /** 46 * @class <tt>R.storage.BrowserStorage</tt> is a generalized class for browser-based 47 * storage mechanisms. Either of the browser storage objects can be accessed using 48 * a SQL-like syntax, with table creation and data manipulation, or using simple 49 * keys and values. 50 * 51 * @param name {String} The name of the object 52 * @extends R.storage.AbstractDBStorage 53 * @constructor 54 * @description Generalized base storage class for browser storage objects. 55 */ 56 R.storage.BrowserStorage = function () { 57 return R.storage.AbstractDBStorage.extend(/** @scope R.storage.BrowserStorage.prototype */{ 58 59 trimPath:null, 60 schema:null, 61 fnv:null, 62 63 /** @private */ 64 constructor:function (name) { 65 this.setStorageObject(this.initStorageObject()); 66 this.fnv = R.util.FNV1Hash.create(); 67 this.base(name); 68 69 // See if a table schema exists for the given name 70 var schema = JSON.parse(this.getStorageObject().getItem(this.getName() + ":schema")); 71 if (schema != null) { 72 // Load the table data 73 var tSchema = {}; 74 for (var s in schema) { 75 tSchema[schema[s]] = this.getTableDef(schema[s]); 76 } 77 this.setSchema(tSchema); 78 79 // We'll update this as needed 80 this.trimPath = TrimPath.makeQueryLang(this.getSchema()); 81 } 82 }, 83 84 /** 85 * A unique identifier for the table name. 86 * @param name {String} The table name 87 * @return {String} A unique identifier 88 */ 89 getTableUID:function (name) { 90 var uid = this.fnv.getHash(this.getName() + name); 91 return uid; 92 }, 93 94 /** 95 * Save a value to the browser storage object. 96 * @param key {String} The key to store the data with 97 * @param value {Object} The value to store with the key 98 */ 99 save:function (key, value) { 100 this.getStorageObject().setItem(this.getTableUID(key) + ":" + key, JSON.stringify(value)); 101 }, 102 103 /** 104 * Get the value associated with the key from the browser storage object. 105 * @param key {String} The key to retrieve data for 106 * @param [defaultValue] {Object} If the value isn't found in storage, use this default value 107 * @return {Object} The value that was stored with the key, or <tt>null</tt> 108 */ 109 load:function (key, defaultValue) { 110 var value = JSON.parse(this.getStorageObject().getItem(this.getTableUID(key) + ":" + key)); 111 if (value === null || value === undefined) { 112 value = defaultValue; 113 } 114 return value; 115 }, 116 117 /** 118 * Get all of the keys associated with this storage object. 119 * @return {Array} An array of key names 120 */ 121 getKeys:function () { 122 var key, keys = [], l = this.getStorageObject().length; 123 while (l > 0) { 124 key = this.getStorageObject().key(--l); 125 var actual = key.split(":")[1]; 126 if (key.indexOf(this.getTableUID(actual)) == 0) { 127 keys.push(actual); 128 } 129 } 130 return keys; 131 }, 132 133 /** 134 * Create a new table to store data in. 135 * 136 * @param name {String} The name of the table 137 * @param columns {Array} An array of case-sensitive column names 138 * @param types {Array} An array of the columns types. The types are 1:1 for the column 139 * names. If you omit <tt>types</tt>, all columns will be assumed type "String". 140 * @return {Boolean} <code>true</code> if the table was created. <code>false</code> if 141 * the table already exists or couldn't be created for another reason. 142 */ 143 createTable:function (name, columns, types) { 144 if (!this.enabled) { 145 return false; 146 } 147 148 try { 149 if (!this.tableExists(name)) { 150 var tName = this.getTableUID(name); 151 152 // Create the schema object 153 var def = {}; 154 for (var c in columns) { 155 def[columns[c]] = { 156 type:types ? types[c] : "String" 157 }; 158 } 159 this.getStorageObject().setItem(tName + ":def", JSON.stringify(def)); 160 this.getStorageObject().setItem(tName + ":dat", JSON.stringify([])); 161 162 // Add it to the overall schema 163 var schema = this.getSchema(); 164 if (schema != null) { 165 schema.push(name); 166 } 167 else { 168 schema = [name]; 169 } 170 this.getStorageObject().setItem(this.getName() + ":schema", JSON.stringify(schema)); 171 172 if (!this.getSchema()) { 173 this.setSchema({}); 174 } 175 this.getSchema()[name] = def; 176 this.trimPath = TrimPath.makeQueryLang(this.getSchema()); 177 178 // Make sure the underlying system is updated 179 this.base(name, def); 180 return true; 181 } 182 else { 183 return false; 184 } 185 } 186 catch (ex) { 187 return false; 188 } 189 }, 190 191 /** 192 * Drop a table by its given name 193 * 194 * @param name {String} The name of the table to drop 195 */ 196 dropTable:function (name) { 197 if (!this.enabled) { 198 return; 199 } 200 201 var tName = this.getTableUID(name); 202 this.getStorageObject().removeItem(tName + ":def"); 203 this.getStorageObject().removeItem(tName + ":dat"); 204 205 // Remove it from the overall schema 206 var schema = this.getSchema(); 207 if (schema != null) { 208 R.engine.Support.arrayRemove(schema, name); 209 } 210 else { 211 schema = []; 212 } 213 if (schema.length == 0) { 214 this.getStorageObject().removeItem(this.getName() + ":schema"); 215 } 216 else { 217 this.getStorageObject().setItem(this.getName() + ":schema", JSON.stringify(schema)); 218 } 219 220 this.base(name); 221 this.trimPath = TrimPath.makeQueryLang(this.getSchema()); 222 }, 223 224 /** 225 * Returns <tt>true</tt> if the table with the given name exists 226 * @param name {String} The name of the table 227 * @return {Boolean} 228 */ 229 tableExists:function (name) { 230 if (!this.enabled) { 231 return false; 232 } 233 234 // See if the table exists 235 var tName = this.getTableUID(name); 236 return !!this.getStorageObject().getItem(tName + ":def"); 237 }, 238 239 /** 240 * Set the data, for the given table, in the persistent storage. 241 * 242 * @param name {String} The name of the table 243 * @param data {Object} The table data to store 244 * @return {Number} 1 if the data was stored, or 0 if the table doesn't exist 245 */ 246 setTableData:function (name, data) { 247 if (!this.enabled) { 248 return 0; 249 } 250 251 // See if the table exists 252 if (this.tableExists(name)) { 253 var tName = this.getTableUID(name); 254 this.getStorageObject().setItem(tName + ":dat", JSON.stringify(data)); 255 return 1; 256 } 257 else { 258 return 0; 259 } 260 }, 261 262 /** 263 * Get the schema object, for the given table. 264 * @param name {String} The name of the table 265 * @return {Object} The data object, or <tt>null</tt> if no table with the given name exists 266 */ 267 getTableDef:function (name) { 268 if (!this.enabled) { 269 return null; 270 } 271 272 // See if the table exists 273 if (this.tableExists(name)) { 274 var tName = this.getTableUID(name); 275 return JSON.parse(this.getStorageObject().getItem(tName + ":def")); 276 } 277 else { 278 return null; 279 } 280 }, 281 282 /** 283 * Get the data object, for the given table. 284 * @param name {String} The name of the table 285 * @return {Object} The data object, or <tt>null</tt> if no table with the given name exists 286 */ 287 getTableData:function (name) { 288 if (!this.enabled) { 289 return null; 290 } 291 292 // See if the table exists 293 if (this.tableExists(name)) { 294 try { 295 var tName = this.getTableUID(name); 296 return JSON.parse(this.getStorageObject().getItem(tName + ":dat")); 297 } catch (ex) { 298 // Most likely "undefined", return an empty object 299 return {}; 300 } 301 } 302 else { 303 return null; 304 } 305 }, 306 307 /** 308 * Get the size of a table's data in bytes. 309 * @param name {String} The name of the table 310 * @return {Number} The size of the table 311 */ 312 getTableSize:function (name) { 313 if (!this.enabled) { 314 return null; 315 } 316 317 // See if the table exists 318 if (this.tableExists(name)) { 319 try { 320 var tName = this.getTableUID(name); 321 return this.getStorageObject().getItem(tName + ":dat").length; 322 } catch (ex) { 323 // Most likely "undefined", return an empty object 324 return 0; 325 } 326 } 327 else { 328 return 0; 329 } 330 }, 331 332 /** 333 * Execute SQL on the storage object, which may be one of <tt>SELECT</tt>, 334 * <tt>UPDATE</tt>, <tt>INSERT</tt>, or <tt>DELETE</tt>. This mechanism allows for 335 * joining of data, querying across multiple tables, and more. 336 * 337 * @param sqlString {String} The SQL to execute 338 * @param bindings {Array} An optional array of bindings 339 * @return {Object} If the SQL is a <tt>SELECT</tt>, the object will be the result of 340 * the statement, otherwise the result will be a <tt>Boolean</tt> if the statement was 341 * successful. 342 */ 343 execSql:function (sqlString, bindings) { 344 if (this.trimPath != null) { 345 // Compile the method 346 var stmt = this.trimPath.parseSQL(sqlString, bindings); 347 // Build an object with all of the data 348 var schema = this.getSchema(); 349 var db = {}; 350 for (var s in schema) { 351 db[s] = this.getTableData(s); 352 } 353 if (sqlString.indexOf("SELECT") != -1) { 354 return stmt.filter(db); 355 } 356 else { 357 // Determine which table was modified 358 var result = stmt.filter(db); 359 var tableName = ""; 360 if (result === true) { 361 // Only update the storage if the statement was successful 362 if (sqlString.indexOf("INSERT") != -1) { 363 tableName = /INSERT INTO (\w*)/.exec(sqlString)[1]; 364 } 365 else if (sqlString.indexOf("UPDATE") != -1) { 366 tableName = /UPDATE (\w*)/.exec(sqlString)[1]; 367 } 368 else { 369 tableName = /DELETE .* FROM (\w*)/.exec(sqlString)[1]; 370 } 371 372 // Extract that table from the database and store it 373 var table = db[tableName]; 374 this.setTableData(tableName, table); 375 } 376 return result; 377 } 378 } 379 } 380 381 }, /** @scope R.storage.BrowserStorage.prototype */ { 382 383 /** 384 * Get the class name of this object 385 * 386 * @return {String} "R.storage.BrowserStorage" 387 */ 388 getClassName:function () { 389 return "R.storage.BrowserStorage"; 390 } 391 392 }); 393 394 };