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