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