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