1 /* ***** BEGIN LICENSE BLOCK ***** 2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 3 * 4 * The contents of this file are subject to the Mozilla Public License Version 5 * 1.1 (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * http://www.mozilla.org/MPL/ 8 * 9 * Software distributed under the License is distributed on an "AS IS" basis, 10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 11 * for the specific language governing rights and limitations under the 12 * License. 13 * 14 * The Original Code is Thunderbird Conversations 15 * 16 * The Initial Developer of the Original Code is 17 * Jonathan Protzenko <jonathan.protzenko@gmail.com> 18 * Portions created by the Initial Developer are Copyright (C) 2010 19 * the Initial Developer. All Rights Reserved. 20 * 21 * Contributor(s): 22 * 23 * Alternatively, the contents of this file may be used under the terms of 24 * either the GNU General Public License Version 2 or later (the "GPL"), or 25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 26 * in which case the provisions of the GPL or the LGPL are applicable instead 27 * of those above. If you wish to allow use of your version of this file only 28 * under the terms of either the GPL or the LGPL, and not to allow others to 29 * use your version of this file under the terms of the MPL, indicate your 30 * decision by deleting the provisions above and replace them with the notice 31 * and other provisions required by the GPL or the LGPL. If you do not delete 32 * the provisions above, a recipient may use your version of this file under 33 * the terms of any one of the MPL, the GPL or the LGPL. 34 * 35 * ***** END LICENSE BLOCK ***** */ 36 37 /** 38 * @fileoverview This file exports the SimpleStorage wrapper around mozStorage. 39 * It is designed to help you use mozStorage is a simple get/set/has/remove 40 * API. 41 * @author Jonathan Protzenko 42 */ 43 44 var EXPORTED_SYMBOLS = ['SimpleStorage'] 45 46 const Ci = Components.interfaces; 47 const Cc = Components.classes; 48 const Cu = Components.utils; 49 const Cr = Components.results; 50 51 Cu.import("resource://conversations/log.js"); 52 let Log = setupLogging("Conversations.SimpleStorage"); 53 Log.debug("Simple Storage loaded."); 54 55 let gStorageService = Cc["@mozilla.org/storage/service;1"] 56 .getService(Ci.mozIStorageService); 57 let gDbFile = Cc["@mozilla.org/file/directory_service;1"] 58 .getService(Ci.nsIProperties) 59 .get("ProfD", Ci.nsIFile); 60 gDbFile.append("simple_storage.sqlite"); 61 62 /** 63 * The global SimpleStorage object. It has various method to instanciate a 64 * storage session with a given style. You should not have two different styles 65 * of API open at the same time on the same table. 66 * @namespace 67 */ 68 let SimpleStorage = { 69 /** 70 * Probably the easiest style to use. Function just take a callback (or 71 * continuation) that will be called once the asynchronous storage operation 72 * is done. 73 * @param {String} aTblName The table name you wish to use. You can prefix it 74 * with your extension's GUID since it will be shared by all extensions 75 * running on the same profile. 76 * @returns {SimpleStorageCps} 77 */ 78 createCpsStyle: function _SimpleStorage_createCps (aTblName) { 79 return new SimpleStorageCps(aTblName); 80 }, 81 82 /** 83 * This is another version of the API that offers the appearance of a 84 * synchronous API. Basically, a call to get now returns a function that takes 85 * one argument, that is, the function that it is expected to call to restart 86 * the original computation. 87 * You are to use it like this: 88 * 89 * <pre> 90 * let ss = SimpleStorage.createIteratorStyle("my-tbl"); 91 * SimpleStorage.spin(function anon () { 92 * let r = yield ss.get("myKey"); 93 * // do stuff with r 94 * yield SimpleStorage.kWorkDone; 95 * // nothing is ever executed after the final yield call 96 * }); 97 * </pre> 98 * 99 * What happens is the anon function is suspended as soon as it yields. If we 100 * call f the function returned by ss.get("myKey"), then spin is the driver 101 * that will run f. Spin passes a function called "finish" to f, and once f 102 * is done fetching the data asynchronously, it calls finish with the result 103 * of its computation. 104 * finish then restarts the anon function with the result of the yield call 105 * being the value f just passed it. 106 * 107 * @param {String} aTblName 108 * @returns {SimpleStorageIterator} 109 */ 110 createIteratorStyle: function _SimpleStorage_createForIterator (aTblName) { 111 let cps = new SimpleStorageCps(aTblName); 112 return new SimpleStorageIterator(cps); 113 }, 114 115 /** 116 * @TODO 117 */ 118 createPromisesStyle: function _SimpleStorage_createPromisesStyle (aTblName) { 119 let cps = new SimpleStorageCps(aTblName); 120 return new SimpleStoragePromises(cps); 121 }, 122 123 kWorkDone: kWorkDone, 124 125 /** 126 * The main driver function for the iterator-style API. 127 */ 128 spin: function _SimpleStorage_spin(f) { 129 let iterator = f(); 130 // Note: As a point of interest, calling send(undefined) is equivalent 131 // to calling next(). However, starting a newborn generator with any value 132 // other than undefined when calling send() will result in a TypeError 133 // exception. 134 (function send(r) { 135 let asyncFunction = iterator.send(r); 136 if (asyncFunction !== kWorkDone) { 137 asyncFunction(function (r) { 138 send(r); 139 }); 140 } 141 })(); 142 }, 143 }; 144 145 /** 146 * You should not instanciate this class directly. Use {@link SimpleStorage.createCpsStyle}. 147 * @constructor 148 */ 149 function SimpleStorageCps(aTblName) { 150 // Will also create the file if it does not exist 151 this.dbConnection = gStorageService.openDatabase(gDbFile); 152 if (!this.dbConnection.tableExists(aTblName)) 153 this.dbConnection.executeSimpleSQL( 154 "CREATE TABLE #1 (key TEXT PRIMARY KEY, value TEXT)".replace("#1", aTblName) 155 ); 156 this.tableName = aTblName; 157 } 158 159 SimpleStorageCps.prototype = { 160 /** 161 * Find the data associated to the given key. 162 * @param {String} aKey The key used to identify your data. 163 * @param {Function} k A function that expects the javascript object you 164 * initially stored, or null. 165 */ 166 get: function _SimpleStorage_get (aKey, k) { 167 let statement = this.dbConnection 168 .createStatement("SELECT value FROM #1 WHERE key = :key".replace("#1", this.tableName)); 169 statement.params.key = aKey; 170 let results = []; 171 statement.executeAsync({ 172 handleResult: function(aResultSet) { 173 for (let row = aResultSet.getNextRow(); 174 row; 175 row = aResultSet.getNextRow()) { 176 let value = row.getResultByName("value"); 177 results.push(value); 178 } 179 }, 180 181 handleError: function(aError) { 182 Log.error("Error:", aError.message); 183 Log.error("Query was get("+aKey+")"); 184 }, 185 186 handleCompletion: function(aReason) { 187 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) { 188 Log.error("Query canceled or aborted!"); 189 Log.error("Query was get("+aKey+")"); 190 } else { 191 if (results.length > 1) { 192 Log.assert(false, "Multiple rows for the same primary key? That's impossible!"); 193 } else if (results.length == 1) { 194 k(JSON.parse(results[0]).value); 195 } else if (results.length == 0) { 196 k(null); 197 } 198 } 199 } 200 }); 201 }, 202 203 /** 204 * Store data for the given key. It will erase any previous binding if any, 205 * and you'll lose the data previously associated with that key. 206 * @param {String} aKey The key. 207 * @param {Object} aVal The value that is to be associated with the key. 208 * @param {Function} k A function that expects one argument and will be called 209 * when the data is stored. The argument is true if the row was added, false 210 * if it was just updated. 211 */ 212 set: function _SimpleStorage_set (aKey, aValue, k) { 213 this.has(aKey, (function (aResult) { 214 let query = aResult 215 ? "UPDATE #1 SET value = :value WHERE key = :key" 216 : "INSERT INTO #1 (key, value) VALUES (:key, :value)" 217 ; 218 let statement = this.dbConnection.createStatement(query.replace("#1", this.tableName)); 219 statement.params.key = aKey; 220 statement.params.value = JSON.stringify({ value: aValue }); 221 statement.executeAsync({ 222 handleResult: function(aResultSet) { 223 }, 224 225 handleError: function(aError) { 226 Log.error("Error:", aError.message); 227 Log.error("Query was get("+aKey+")"); 228 }, 229 230 handleCompletion: function(aReason) { 231 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) { 232 Log.error("Query canceled or aborted!"); 233 Log.error("Query was get("+aKey+")"); 234 } else { 235 k(!aResult); 236 } 237 } 238 }); 239 }).bind(this)); 240 }, 241 242 /** 243 * Check whether there is data associated to the given key. 244 * @param {String} aKey The key. 245 * @param {Function} k A function that expects a boolean. 246 */ 247 has: function _SimpleStorage_has (aKey, k) { 248 this.get(aKey, function (aVal) { 249 k(aVal != null); 250 }); 251 }, 252 253 /** 254 * Remove data associated with the given key. 255 * @param {String} aKey The key. 256 * @param {Function} k A function that expects a boolean telling whether data 257 * was actually removed or not. 258 */ 259 remove: function _SimpleStorage_remove (aKey, k) { 260 this.has(aKey, (function (aResult) { 261 if (!aResult) { 262 k(false); // element was not removed 263 } else { 264 let query = "DELETE FROM #1 WHERE key = :key"; 265 let statement = this.dbConnection.createStatement(query.replace("#1", this.tableName)); 266 statement.params.key = aKey; 267 statement.executeAsync({ 268 handleResult: function(aResultSet) { 269 }, 270 271 handleError: function(aError) { 272 Log.error("Error:", aError.message); 273 Log.error("Query was get("+aKey+")"); 274 }, 275 276 handleCompletion: function(aReason) { 277 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) { 278 Log.error("Query canceled or aborted!"); 279 Log.error("Query was get("+aKey+")"); 280 } else { 281 k(true); // element was removed 282 } 283 } 284 }); 285 } 286 }).bind(this)); 287 }, 288 } 289 290 /** 291 * You should not instanciate this class directly. Use {@link SimpleStorage.createIteratorStyle}. 292 * @constructor 293 */ 294 function SimpleStorageIterator(aSimpleStorage) { 295 this.ss = aSimpleStorage; 296 } 297 298 SimpleStorageIterator.prototype = { 299 300 get: function _SimpleStorage_get (aKey) (function (finish) { 301 this.get(aKey, function (result) finish(result)); 302 }).bind(this.ss), 303 304 set: function _SimpleStorage_set (aKey, aValue) (function (finish) { 305 this.set(aKey, aValue, function (result) finish(result)); 306 }).bind(this.ss), 307 308 has: function _SimpleStorage_has (aKey) (function (finish) { 309 this.has(aKey, function (result) finish(result)); 310 }).bind(this.ss), 311 312 remove: function _SimpleStorage_remove (aKey) (function (finish) { 313 this.remove(aKey, function (result) finish(result)); 314 }).bind(this.ss), 315 316 } 317