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