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 };