1 2 /** @fileOverview Easy way to persist data between requests.<br /> 3 * <p>Importing the "storage" module gives you access to a root 4 * <code>storage</code> object. Any property you assign to this object 5 * in one request will be avialable in any subsequent requests. 6 * <p>The typical way to add a top-level property of 7 * <code>storage</code> is to check if it's defined and – if not 8 * – set it to some default value, as in the example below. 9 * @example 10 import("storage"); 11 12 if (! storage.counter) { 13 storage.counter = 0; 14 } 15 */ 16 17 var _a = _appjetnative_; 18 19 function _Storage() { 20 this.get = function(key) { 21 return _a.storage_get(key); 22 } 23 this.put = function(key, value) { 24 _a.storage_put(key, value) 25 } 26 this.erase = function(key) { 27 _a.storage_delete(key); 28 } 29 } 30 31 var _s = new _Storage(); 32 // var debugstorage = _s; 33 34 _radixChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 35 _radix = _radixChars.length 36 37 function _longToString(n) { 38 if (n < _radix) return _radixChars[n]; 39 else return _longToString(Math.floor(n/_radix)) + _radixChars[n%_radix]; 40 } 41 42 function _newUniqueId(prefix) { 43 return prefix + _longToString(appjet._native.storage_time()) + _longToString(Math.floor(Math.random()*_radix*_radix)); 44 } 45 46 function _proxy(id) { 47 if (id.slice(0, 5) == "coll-") { 48 return _a.createProxyObject(_coll_getHandler(id), _coll_setHandler(id), _coll_deleteHandler(id), 49 _coll_getIdsHandler(id), StorableCollection.prototype); 50 } else if (id.slice(0, 4) == "obj-") { 51 return _a.createProxyObject(_getHandler(id), _setHandler(id), _deleteHandler(id), 52 _getIdsHandler(id), StorableObject.prototype); 53 } else { 54 throw new Error("Unknown type for object id"); 55 } 56 } 57 58 /** 59 * Gets a StorableObject or StorableCollection by id. <!-- This 60 * function is useful for passing "objects" between user requests; for 61 * example, to modify an object using a form, just pass the id of the 62 * object to the posted page in a hidden input element, then retrieve 63 * it on the posted page using this function. See the example 64 * below. --> 65 * @param {string} id The id of the object to be gotten. 66 * @example 67 function get_editor() { 68 // Objects passed between requests must be reachable from storage. 69 var obj = storage.users.iterator().next(); 70 71 // In your edit page; "obj" is the object to edit. 72 print(FORM({action: "/doedit", method: "post"}, 73 INPUT({type: "text", name: "title"}), 74 INPUT({type: "hidden", name: "id", value: obj.id}), 75 INPUT({type: "submit", name: "edit"}))); 76 } 77 78 function post_doedit() { 79 // In your form's action page. 80 81 getStorable(request.param("id")).title = request.param("title"); 82 } 83 * @return {StorableObject} The StorableObject whose id is <code>id</code>, 84 * or <code>undefined</code> if no such object exists. 85 */ 86 function getStorable(id) { 87 if (_s.get(id+":") != undefined) 88 return _getObjectForId(id); 89 } 90 91 function _coerceToHolder(obj) { 92 if (typeof(obj) == 'function') 93 return false; 94 if (typeof(obj) != 'object') 95 return {t: typeof(obj), v: ""+obj} 96 if (obj instanceof String) 97 return {t: 'string', v: ""+obj}; 98 if (obj instanceof Date) 99 return {t: 'date', v: ""+obj.getTime()} 100 if (obj instanceof Number) 101 return {t: 'number', v: ""+obj} 102 if (obj instanceof Boolean) 103 return {t: 'boolean', v: ""+obj} 104 if (obj instanceof StorableObject) 105 return {t: 'object', v: obj.id} 106 return false 107 } 108 109 function _coerceToStorableHolder(obj) { 110 // _print("Coercing to storableHolder: "+obj); 111 var test = _coerceToHolder(obj); 112 if (test) { 113 // _print("...coerced to plain holder: "+test) 114 return test; 115 } 116 // _print("...failed to coerce to holder, trying object") 117 return {t: 'object', v: _coerceToStorableObject(obj).id} 118 } 119 120 function _coerceToStorableObject(obj) { 121 // _print("Coercing to storableObject: "+obj) 122 123 if (typeof(obj) != 'object') 124 throw new TypeError(""+obj+" could not be made storable!"); 125 126 if (obj instanceof StorableObject) 127 return obj; 128 129 if (obj instanceof Array) 130 throw new TypeError("Can't store an array; use a StorableCollection instead.") 131 132 eachProperty(obj, function(k, v) { 133 // _print("...testing each property; now: "+k+", value: "+v); 134 if (k == 'id' || k == 'toString' || k == 'hasOwnProperty' || (! _coerceToStorableHolder(v))) 135 throw new TypeError("Couldn't make object storable; property "+k+" failed to convert ("+v+")"); 136 }); 137 138 var ret = new StorableObject(); 139 eachProperty(obj, function(k, v) { 140 // bypass the usual setting mechanism, since we have a holder here. 141 _setHandler(ret.id, true)(k, _coerceToStorableHolder(v)); 142 }); 143 144 return ret; 145 } 146 147 function _print(msg) { 148 var _debug = false; 149 if (_debug) 150 print(msg); 151 } 152 153 var _idMap = {}; 154 function _getObjectForId(id) { 155 if (id == null || id == undefined) 156 throw new Error("Bad ID: "+id) 157 if (!(id in _idMap)) { 158 // _print("...wasn't available, making new"); 159 var o = _proxy(id); 160 // _print("...o is: "+o.id); 161 _idMap[id] = o; 162 // _print("...id is in _idMap? "+(id in _idMap)); 163 var v = _idMap[id]; 164 // _print("...v is: "+typeof(v)); 165 } 166 // _print("...and returning: "+_idMap[id].id); 167 return _idMap[id]; 168 } 169 170 function _iteratingToString(obj) { 171 var visited = {}; visited[obj.id] = true; 172 var helper = function(obj) { 173 var s = "S{ "; 174 var props = [ "id: "+obj.id ]; 175 for (var i in obj) { 176 if (obj[i] instanceof StorableObject) { 177 if (visited[obj[i].id] == true) { 178 props.push(i+": [seen "+obj[i].id+"]"); 179 } else { 180 visited[obj[i].id] = true; 181 props.push(i+": "+(obj[i] instanceof StorableCollection ? obj[i] : helper(obj[i]))); 182 } 183 } else { 184 props.push(i+": "+obj[i]); 185 } 186 } 187 s += props.join(", ") + " }"; 188 return s; 189 } 190 return helper(obj); 191 } 192 193 function _getHandler(id) { 194 return function(name) { 195 // _print("on object id: "+id+", request for property: "+name); 196 if (name == "id") 197 return id; 198 if (name == "toString") { 199 return function() { return _iteratingToString(this) }; 200 } 201 if (name == "toHTML") { 202 return function() { 203 var t = TABLE({border: 1, cellpadding: 2, cellspacing: 0}); 204 t.push(TR(TD("id"), TD(id))); 205 eachProperty(this, function(name, value) { 206 t.push(TR(TD(String(name)), TD(String(value)))); 207 }); 208 return toHTML(t); 209 } 210 } 211 if (name == "hasOwnProperty") { 212 return function(name) { 213 // XXX: This should be in ".has" 214 return (! (_s.get(id+"/"+name) === undefined)); 215 } 216 } 217 eval("var o = "+_s.get(id+"/"+name)); 218 if (o == undefined) 219 return undefined; 220 switch (o.t) { 221 case 'number': 222 return Number(o.v); 223 case 'boolean': 224 return (o.v == "true" ? true : false); 225 case 'undefined': 226 return undefined; 227 case 'date': 228 return new Date(Number(o.v)); 229 case 'object': 230 return _getObjectForId(o.v); 231 default: 232 // _print("for id "+id+" getting property "+name+" returning "+o.v); 233 return o.v; 234 } 235 } 236 } 237 238 function _setHandler(id, raw) { 239 return function(name, value) { 240 if (name == "id") 241 throw new Error("Can't set id property."); 242 if (name == "toString") 243 throw new Error("Can't set toString property."); 244 if (name == "hasOwnProperty") 245 throw new Error("Can't set hasOwnProperty property."); 246 var old = _getIdsHandler(id)(); 247 248 if (old == undefined) 249 old = [] 250 var exists = false; 251 for (var i = 0; i < old.length; ++i) { 252 if (old[i] == name) { 253 exists = true; 254 break; 255 } 256 } 257 258 var newValue = (raw ? value : _coerceToStorableHolder(value)) 259 260 var oldValue 261 if (! exists) { 262 old.push(name); 263 _s.put(id+":", ""+old); 264 } else { 265 eval("oldValue = "+_s.get(id+"/"+name)); 266 } 267 268 _s.put(id+"/"+name, ""+newValue); 269 } 270 } 271 272 function _deleteHandler(id) { 273 return function(name) { 274 eval("var oldValue = "+_s.get(id+"/"+name)); 275 _s.erase(id+"/"+name); 276 eval("var old = "+_s.get(id+":")); 277 old = _removeItem(old, name); 278 _s.put(id+":", ""+old); 279 } 280 } 281 282 function _getIdsHandler(id) { 283 return function() { 284 eval("var old = "+_s.get(id+":")); 285 if (old == undefined) 286 old = []; 287 // _print("for id "+id+", returning ids: "+old); 288 return old; 289 } 290 } 291 292 293 /** 294 * An object that can be stored directly, and is backed by our 295 * database. StorableObjects can be added to StorableCollections. 296 * @constructor @param {object} [obj] An optional object argument 297 * whose properties will be copied into the newly-created 298 * StorableObject. <code>obj</code> cannot have any functions, and any 299 * properties of <code>obj</code>'s prototype are ignored. Any objects 300 * that <code>obj</code> references will also be copied. (Note that 301 * any subsequent modifications to <code>obj</code> will not be 302 * reflected in the newly-created StorableObject.) 303 * @example 304 var s = new StorableObject(); 305 s.message = "Hi there!"; 306 307 storage.foo = s; 308 * @example 309 var s = new StorableObject({message: "Hi there!"}); 310 311 var c = new StorableCollection(); 312 c.add(s) 313 */ 314 function StorableObject(obj) { 315 if (typeof(obj) == 'object') 316 return _coerceToStorableObject(obj) 317 318 var id = _newUniqueId("obj-"); 319 // _print("making object for id: "+id); 320 _s.put(id+":", ""+[]); 321 322 var o = _getObjectForId(id); 323 // _print("...and it is: "+o.id); 324 return o; 325 } 326 StorableObject.prototype = {} 327 328 /** 329 * The id of a StorableObject is auto-generated. You can get the 330 * object associated with a given id using <code>getStorable</code>. 331 * @name id 332 * @memberOf StorableObject 333 * @type string 334 */ 335 336 var storage = _getObjectForId("obj-root"); 337 338 function _Iterable() { 339 this.i = 0; 340 this.reset = function() { this.i = 0; } 341 342 this.next = function() { 343 if (this.hasNext()) { 344 this.i += 1; 345 return _getObjectForId(this.base[this.i-1]) 346 } 347 } 348 this.hasNext = function() { 349 return ((this.base instanceof Array) && (this.i < this.base.length)) 350 } 351 352 this.forEach = function(cb) { 353 for (var i = 0; i < this.base.length; ++i) { 354 if (cb(_getObjectForId(this.base[i])) === false) { 355 break; 356 } 357 } 358 } 359 this.size = function() { return this.base.length; } 360 this.filter = function(filter_obj) { 361 return new _FilterIterator(this, filter_obj); 362 } 363 this.sort = function(sort_fn) { 364 return new _SortIterator(this, sort_fn); 365 } 366 this.toHTML = function() { return _coll_toHTML(this, this.type); } 367 } 368 var _iterable = new _Iterable(); 369 370 function _Iterator(base, id) { 371 this.type = id; 372 this.base = base; 373 } 374 _Iterator.prototype = _iterable; 375 376 function _FilterIterator(base, filter_obj) { 377 this.type = base.type+".filter("+filter_obj+")"; 378 this.base = [] 379 while (base.hasNext()) { 380 var n = base.next(); 381 if (_match(filter_obj, n)) { 382 this.base.push(n.id); 383 } 384 } 385 } 386 _FilterIterator.prototype = _iterable; 387 388 function _SortIterator(base, sort_fn) { 389 this.type = base.type+".sort("+sort_fn+")"; 390 this.base = [].concat(base.base); 391 this.base.sort(function(ida, idb) { 392 return sort_fn(_getObjectForId(ida), _getObjectForId(idb)); 393 }); 394 } 395 _SortIterator.prototype = _iterable; 396 397 // function _dll_add(id, obj) { 398 // if (!isStorableObject(obj)) throw new TypeError("Not a storable object!"); 399 // eval("var base = "+_s.get(id+"/")); 400 // if (base == undefined) 401 // base = {'start': obj.id, 'end': ""} 402 // var newObj = {prev: base.end, next: ""} 403 // base.end = obj.id 404 // _s.put(id+"/"+obj.id, ""+newObj); 405 // _s.put(id+"/", ""+base); 406 // } 407 408 // function _dll_remove(id, obj) { 409 // if (!_isStorableObject(obj)) throw new TypeError("Not a storable object!"); 410 // eval("var base = "+_s.get(id+"/")); 411 // if (base == undefined) 412 // return; 413 // eval("var ptr = "+_s.get(id+"/"+base.start)); 414 // while (base.end != "") { 415 // if (ptr.id == obj.id) { 416 // if (ptr.prev != "") { 417 // eval("var prev = "+_s.get(id+"/"+ptr.prev)); 418 // prev.next = ptr.next; 419 // _s.put(id+"/"+ptr.prev, ""+prev) 420 // } else { 421 // base.start = ptr.next; 422 // } 423 // if (ptr.next != "") { 424 // eval("var next = "+_s.get(id+"/"+ptr.next)); 425 // next.prev = ptr.prev; 426 // _s.put(id+"/"+ptr.next, ""+next) 427 // } else { 428 // base.end = ptr.prev; 429 // } 430 // _s.put(id+"/", ""+base); 431 // break 432 // } 433 // } 434 // } 435 436 // function _dll_getIds(id, obj) { 437 // return function() { return ['add', 'remove', 'iterator', 'filter', 'sort']; } 438 // } 439 440 /** 441 * A StorableCollection persistently stores other StorableObjects. 442 * @constructor 443 * @extends StorableObject 444 * @inherits {string} StorableObject.id 445 * @example 446 storage.foo = new StorableCollection(); 447 storage.foo.add({name: "John"}); 448 storage.foo.add({name: "Mary"}); 449 450 storage.foo.forEach(printp); 451 */ 452 function StorableCollection() { 453 var id = _newUniqueId("coll-"); 454 _s.put(id+":", "[]"); 455 return _getObjectForId(id); 456 } 457 StorableCollection.prototype = _a.createProxyObject(function() { }, function() { }, function() { }, function() { }, 458 StorableObject.prototype) 459 460 function _match(base, query) { 461 for (var i in base) { 462 if (base[i] != query[i]) 463 return false; 464 } 465 return true; 466 } 467 468 function _removeItem(arr, obj) { 469 var newArr = [] 470 for (var i = 0; i < arr.length; ++i) { 471 if (obj != arr[i]) 472 newArr.push(arr[i]); 473 } 474 return newArr; 475 } 476 477 /** 478 * Adds a StorableObject to this collection. 479 * @name add 480 * @function 481 * @memberOf StorableCollection 482 * @param {object} obj The object to add to this collection. If 483 * <code>obj</code> is not a StorableObject, a new StorableObject is 484 * created from <code>obj</code> by passing it to the StorableObject 485 * constructor, copying its properties. 486 * @example 487 var c = new StorableCollection(); 488 c.add({name: "John"}); 489 */ 490 function _coll_add(id, o) { 491 if (typeof(o) != 'object') 492 throw new TypeError("Not a storable object!"); 493 494 // Option to do an "addAll"; an iterable can't be part of a collection. 495 if (o instanceof _Iterable) { 496 o.forEach(function(obj) { _coll_add(id, obj) }); 497 return true; 498 } 499 500 var obj = _coerceToStorableObject(o) 501 502 // update self 503 eval("var base = "+_s.get(id+":")); 504 if (base == undefined) 505 base = []; 506 base.push(obj.id); 507 _s.put(id+":", ""+base); 508 return obj; 509 } 510 511 /** 512 * Removes StorableObjects from this collection. 513 * @name remove 514 * @function 515 * @memberOf StorableCollection 516 * @param {object} obj The object to remove from this collection. If 517 * <code>obj</code> is a StorableObject, <code>obj</code> itself is 518 * removed from this collection. If <code>obj</code> is an collection 519 * or a view on a collection (such as one created by <a 520 * href="#StorableCollection.filter">filter</a>), then all objects 521 * provided in that collection are removed. Finally, if 522 * <code>obj</code> is just a plain object, then all members of this 523 * collection that have the same properties and values as 524 * <code>obj</code> are removed. Note: passing an <code>{}</code> 525 * removes <em>all</em> objects form this collection. 526 * @example 527 var c = storage.users; // a StorableCollection 528 c.remove({name: "John"}); 529 * @see #StorableCollection.filter 530 */ 531 function _coll_remove(id, obj) { 532 if (typeof(obj) != 'object') 533 throw new TypeError("Not a storable object!"); 534 535 // A "removeAll"; an iterable can't be part of a collection. 536 if (obj instanceof _Iterable) { 537 obj.forEach(function(o) { _coll_remove(id, o) }); 538 return; 539 } 540 if (! (obj instanceof StorableObject)) { 541 _coll_remove(id, _coll_filter(id, obj)); 542 return; 543 } 544 545 eval("var base = "+_s.get(id+":")); 546 if (base == undefined) 547 base = []; 548 var newBase = []; 549 for (var i = 0; i < base.length; ++i) { 550 if (base[i] != obj.id) 551 newBase.push(base[i]); 552 } 553 _s.put(id+":", ""+newBase); 554 } 555 556 function _coll_iterator(id) { 557 eval("var base = "+_s.get(id+":")); 558 if (base == undefined) 559 base = []; 560 var i = 0; 561 return new _Iterator(base, id) 562 } 563 564 /** 565 * Filters this collection based on a matching object. 566 * @name filter 567 * @scope StorableCollection 568 * @function 569 * @memberOf StorableCollection 570 * @param {object} match The object to base a filter on. The returned 571 * view contains only those members of this StorableCollection whose 572 * properties have the same value as the ones in 573 * <code>match</code>. (Properties in the collection's members not 574 * present in <code>match</code> are ignored.) <code>filter</code> and 575 * <code>sort</code> are "chainable" operations; they can be applied 576 * to filtered and sorted views. 577 * @example 578 var c = new StorableCollection(); 579 c.add(new StorableObject({first_name: "Bob", last_name: "Smith"})); 580 c.add(new StorableObject({first_name: "John", last_name: "Smith"})); 581 582 var i = c.filter({first_name: "John"}); 583 i.forEach(printp) // prints John Smith's entry above. 584 * @example 585 // Filter and sort are chainable, as in this example: 586 var c = storage.users; // a StorableCollection 587 588 var view = 589 c.filter({first_name: "John"}).sort(function(a, b) { return a.age - b.age }) 590 view.forEach(printp) // prints users named John, sorted from youngest to oldest. 591 * @return A filtered view of this collection. 592 */ 593 function _coll_filter(id, match) { return new _FilterIterator(_coll_iterator(id), match) } 594 595 /** 596 * Sorts this collection based on a sorting function. 597 * @name sort 598 * @function 599 * @memberOf StorableCollection 600 * @param {function} compare As with the function argument to 601 * Array.sort, <code>compare</code> should take two arguments 602 * <em>a</em> and <em>b</em>, and return a negative value, 0, or a 603 * positive value, if <em>a</em> < <em>b</em>, <em>a</em> = 604 * <em>b</em>, or <em>a</em> > <em>b</em>, 605 * respectively. <code>filter</code> and <code>sort</code> are 606 * "chainable" operations; they can be applied to filtered and sorted 607 * views. 608 * @example 609 c.sort(function(a, b) { return a.date - b.date }) 610 * @return A sorted view of this collection. 611 */ 612 function _coll_sort(id, compare) { return new _SortIterator(_coll_iterator(id), compare) } 613 614 /** 615 * Executes a function once on each member of this collection. Note 616 * that forEach can only be called once on a view (though it can be 617 * called multiple times on a StorableCollection). 618 * @name forEach 619 * @function 620 * @memberOf StorableCollection 621 * @param {function} f The function to call on each member of this 622 * collection. Returning <code>false</code> will cause forEach to abort. 623 */ 624 625 /** 626 * Returns the number of elements in a collection. Can also be applied 627 * to filtered and sorted views of a collection. (This number may be 628 * approximate if your collection is very large.) 629 * @name size 630 * @function 631 * @m