1 // ==========================================================================
  2 // Project:   The M-Project - Mobile HTML5 Application Framework
  3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
  4 // Creator:   Sebastian
  5 // Date:      15.11.2010
  6 // License:   Dual licensed under the MIT or GPL Version 2 licenses.
  7 //            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
  8 //            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
  9 // ==========================================================================
 10 
 11 m_require('core/datastore/data_provider.js');
 12 
 13 /**
 14  * @class
 15  * 
 16  * Encapsulates access to LocalStorage (in-browser key value store).
 17  * LocalStorage is an in-browser key-value store to persist data.
 18  * This data provider persists model records as JSON strings with their name and id as key.
 19  * When fetching these strings from storage, their automatically converted in their corresponding model records.
 20  *
 21  * @extends M.DataProvider
 22  */
 23 M.LocalStorageProvider = M.DataProvider.extend(
 24 /** @scope M.LocalStorageProvider.prototype */ {
 25 
 26     /**
 27      * The type of this object.
 28      * @type String
 29      */
 30     type: 'M.LocalStorageProvider',
 31 
 32     /**
 33      * Saves a model record to the local storage
 34      * The key is the model record's name combined with id, value is stringified object
 35      * e.g.
 36      * Note_123 => '{ text: 'buy some food' }'
 37      *
 38      * @param {Object} that (is a model).
 39      * @returns {Boolean} Boolean indicating whether save was successful (YES|true) or not (NO|false).
 40      */
 41     save: function(obj) {
 42         try {
 43             console.log(obj);
 44             localStorage.setItem(obj.model.name + '_' + obj.model.id, JSON.stringify(obj.model.record));
 45             return YES;
 46         } catch(e) {
 47             M.Logger.log(M.WARN, 'Error saving ' + obj.model.record + ' to localStorage with ID: ' + obj.model.name + '_' + that.id);
 48             return NO;
 49         }
 50 
 51     },              
 52 
 53     /**
 54      * deletes a model from the local storage
 55      * key defines which one to delete
 56      * e.g. key: 'Note_123'
 57      *
 58      * @param {Object} obj The param obj, includes model
 59      * @returns {Boolean} Boolean indicating whether save was successful (YES|true) or not (NO|false).
 60      */
 61     del: function(obj) {
 62         try {
 63             if(localStorage.getItem(obj.model.name + '_' + obj.model.id)){ // check if key-value pair exists
 64                 localStorage.removeItem(obj.model.name + '_' + obj.model.id);
 65                 return YES;
 66             }
 67             return NO;
 68         } catch(e) {
 69             M.Logger.log(M.WARN, 'Error removing ID: ' + obj.model.name + '_' + obj.model.id + ' from localStorage');
 70             return NO;
 71         }
 72     },
 73 
 74     /**
 75      * Finds all models of type defined by modelName that match a key or a simple query.
 76      * A simple query example: 'price < 2.21'
 77      * Right now, no AND or OR joins possible, just one query constraint.
 78      *
 79      * If no query is passed, all models are returned by calling findAll()
 80      * @param {Object} The param object containing e.g. the query or the key.
 81      * @returns {Object|Boolean} Returns an object if find is done with a key, an array of objects when a query is given or no
 82      * parameter passed.
 83      */
 84     find: function(obj) {
 85         if(obj.key) {
 86             console.log('got the key...');
 87             var record = this.findByKey(obj);
 88             if(!record) {
 89                 return NO;
 90             }
 91             /*construct new model record with the saved id*/
 92             var reg = new RegExp('^' + obj.model.name + '_([0-9]+)').exec(obj.key);
 93             var m_id = reg && reg[1] ? reg[1] : null;
 94             if (!m_id) {
 95                 M.Logger.log('retrieved model has no valid key: ' + obj.key, M.ERROR);
 96                 return NO;
 97             }
 98             var m = obj.model.createRecord($.extend(record, {id: m_id, state: M.STATE_VALID}));
 99             return m;
100         }
101 
102         if(obj.query){
103             /**
104              * RegEx to match simple queries. E.g.:
105              * username = 'paul'
106              * price < 12.23
107              * result >= -23
108              * Captures:
109              * 1:   identifier      ( e.g. price )      => (\w*)
110              * 2:   operator        ( e.g. < )          => ([<>!=]{1,2}) (actually !! is also allowed but will result in an error
111              * 3:   value           ( e.g. 12.23 )      => String or Number: (['"]\w*['"]|(-)?\d+(\.\d+)?)
112              */
113             var query_regex = /^\s*(\w*)\s*([<>!=]{1,2})\s*(['"]?\w*['"]?|(-)?\d+(\.\d+)?)\s*$/;
114             var regexec = query_regex.exec(obj.query);
115             if(regexec) {
116                 var ident = regexec[1];
117                 var op = regexec[2];
118                 var val = regexec[3].replace(/['"]/g, "");/* delete quotes from captured string, needs to be done in regex*/
119                 var res = this.findAll(obj);
120                 switch(op) {
121                     case '=':
122                         res = _.select(res, function(o){
123                             return o.record[ident] === val;
124                         });
125                         break;
126                     case '!=':
127                         res = _.select(res, function(o){
128                             return o.record[ident] !== val;
129                         });
130                         break;
131                     case '<':
132                         res = _.select(res, function(o){
133                             return o.record[ident] < val;
134                         });
135                         break;
136                     case '>':
137                         res = _.select(res, function(o){
138                             return o.record[ident] > val;
139                         });
140                         break;
141                     case '<=':
142                         res = _.select(res, function(o){
143                             return o.record[ident] <= val;
144                         });
145                         break;
146                     case '>=':
147                         res = _.select(res, function(o){
148                             return o.record[ident] >= val;
149                         });
150                         break;
151                     default:
152                         M.Logger.log('Unknown operator in query: ' + op, M.WARN);
153                         res = [];
154                         break;
155                 }
156             } else{
157                 M.Logger.log('Query does not satisfy query grammar.', M.WARN);
158                 res = [];
159             }
160 
161             return res;
162             
163         } else { /* if no query is passed, all models for modelName shall be returned */
164             return this.findAll(obj);
165         }
166     },
167 
168     /**
169      * Finds a record identified by the key.
170      *
171      * @param {Object} The param object containing e.g. the query or the key.
172      * @returns {Object|Boolean} Returns an object identified by key, correctly built as a model record by calling
173      * or a boolean (NO|false) if no key is given or the key does not exist in LocalStorage.
174      * parameter passed.
175      */
176     findByKey: function(obj) {
177         if(obj.key) {
178             if(localStorage.getItem(obj.key)) { // if key is available
179                 return this.buildRecord(obj.key, obj)
180             } else {
181                 return NO;
182             }
183         }
184         M.Logger.log("Please provide a key.", M.WARN);
185         return NO;
186     },
187 
188     /**
189      * Returns all models defined by modelName.
190      *
191      * Models are saved with key: Modelname_ID, e.g. Note_123
192      *
193      * @param {Object} obj The param obj, includes model
194      * @returns {Object} The array of fetched objects/model records. If no records the array is empty.
195      */
196     findAll: function(obj) {
197         var result = [];
198         for (var i = 0; i < localStorage.length; i++){
199             var k = localStorage.key(i);
200             regexResult = new RegExp('^' + obj.model.name + '_').exec(k);
201             if(regexResult) {
202                 var record = this.buildRecord(k, obj);//JSON.parse(localStorage.getItem(k));
203 
204                 /*construct new model record with the saved id*/
205                 var reg = new RegExp('^' + obj.model.name + '_([0-9]+)').exec(k);
206                 var m_id = reg && reg[1] ? reg[1] : null;
207                 if (!m_id) {
208                     M.Logger.log('Model Record id not correct: ' + m_id, M.ERROR);
209                     continue; // if m_id does not exist, continue with next record element
210                 }
211                 var m = obj.model.createRecord($.extend(record, {id: m_id, state: M.STATE_VALID}));
212                 
213                 result.push(m);
214             }
215         }
216         return result;
217     },
218 
219     /**
220      * Fetches a record from LocalStorage and checks whether automatic parsing by JSON.parse set the elements right.
221      * Means: check whether resulting object's properties have the data type define by their model attribute object.
222      * E.g. String containing a date is automatically transfered into a M.Date object when the model attribute has the data type
223      * 'Date' set for this property.
224      * 
225      * @param {String} key The key to fetch the element from LocalStorage
226      * @param {Object} obj The param object, includes model
227      * @returns {Object} record The record object. Includes all model record properties with correctly set data types.
228      */
229     buildRecord: function(key, obj) {
230         var record = JSON.parse(localStorage.getItem(key));
231         for(var i in record) {
232             if(obj.model.__meta[i] && typeof(record[i]) !== obj.model.__meta[i].dataType.toLowerCase()) {
233                 switch(obj.model.__meta[i].dataType) {
234                     case 'Date':
235                         record[i] = M.Date.create(record[i]);
236                         break;
237                 }
238             }
239         }
240         return record;
241     },
242 
243     /**
244      * Returns all keys for model defined by modelName.
245      *
246      * @param {Object} obj The param obj, includes model
247      * @returns {Object} keys All keys for model records in LocalStorage for a certain model identified by the model's name.
248      */
249     allKeys: function(obj) {
250         var keys = [];
251         for (var i = 0; i < localStorage.length; i++){
252             var k = localStorage.key(i)
253             regexResult = new RegExp('^' + obj.model.name + '_').exec(k);
254             if(regexResult) {
255                 keys.push(k);
256             }
257         }
258         return keys;
259     }
260 
261 });