1 //
  2 // Copyright 2010, Tridium, Inc. All Rights Reserved.
  3 //
  4 
  5 /**
  6  * Collections Architecture for BajaScript.
  7  *
  8  * @author Gareth Johnson
  9  * @version 1.0.0.0
 10  */
 11 
 12 //JsLint options (see http://www.jslint.com )
 13 /*jslint rhino: true, onevar: false, plusplus: true, white: true, undef: false, nomen: false, 
 14 eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false, indent: 2, 
 15 vars: true, continue: true */
 16 
 17 // Globals for JsLint to ignore 
 18 /*global baja, BaseBajaObj*/ 
 19   
 20 (function coll(baja) {
 21 
 22   // Use ECMAScript 5 Strict Mode
 23   "use strict";
 24   
 25   // Create local for improved minification
 26   var strictArg = baja.strictArg,
 27       strictAllArgs = baja.strictAllArgs,
 28       bajaDef = baja.def;
 29   
 30   function isObjEmpty(obj) {
 31     var p;
 32     for (p in obj) {
 33       if (obj.hasOwnProperty(p)) {
 34         return false;
 35       }
 36     }
 37     return true;
 38   }
 39   
 40   /**
 41    * @namespace BajaScript Collection Namespace.
 42    */
 43   baja.coll = new BaseBajaObj();
 44      
 45   ////////////////////////////////////////////////////////////////
 46   // Collection
 47   //////////////////////////////////////////////////////////////// 
 48   
 49   /**
 50    * @class Cursor for a Collection.
 51    * 
 52    * @see baja.coll.Collection
 53    *
 54    * @name CollectionCursor
 55    * @extends baja.Cursor
 56    * @inner
 57    * @public
 58    */
 59   var CollectionCursor = function (collection, curData) {
 60     CollectionCursor.$super.apply(this, arguments);
 61     this.$collection = collection;
 62     this.$curData = curData;
 63     this.$index = -1;
 64     this.$before = baja.noop;
 65     this.$after = baja.noop;
 66   }.$extend(baja.AsyncCursor);
 67   
 68   /**
 69    * Return the underlying Cursor's Collection.
 70    * 
 71    * @returns {baja.coll.BoxCollection}
 72    */
 73   CollectionCursor.prototype.getCollection = function () {
 74     return this.$collection;
 75   }; 
 76   
 77   var colGet = function () {
 78     return this.$curData[this.$index] || null;
 79   };
 80       
 81   /**
 82    * Return the current row.
 83    * 
 84    * @name CollectionCursor#get
 85    * @function
 86    * 
 87    * @returns the cursor value (null if none available)
 88    */
 89   CollectionCursor.prototype.get = colGet; 
 90 
 91   /**
 92    * When the cursor is iterated, the before function will be called
 93    * just before iteration starts.
 94    * <p>
 95    * When the function is called, 'this' refers to the Cursor. The Cursor is also
 96    * passed in as a argument to this function.
 97    * 
 98    * @param {Function} func the before function.
 99    */
100   CollectionCursor.prototype.before = function (func) {
101     strictArg(func, Function);
102     this.$before = func;
103   };
104   
105   /**
106    * When the cursor is iterated, the before function will be called
107    * just after iteration has finished.
108    * <p>
109    * When the function is called, 'this' refers to the Cursor. The Cursor is also
110    * passed in as a argument to this function.
111    * 
112    * @param {Function} func the before function.
113    */
114   CollectionCursor.prototype.after = function (func) {
115     strictArg(func, Function);
116     this.$after = func;
117   };
118   
119   /**
120    * Iterate through the Cursor and call 'each' for every item.
121    * <p>
122    * When the function is called, 'this' refers to the Cursor.
123    * 
124    * @param {Function} func function called on every iteration with 
125    *                        the current row used as the argument.
126    */
127   CollectionCursor.prototype.each = function (func) {
128     strictArg(func, Function);
129         
130     this.$index = -1;
131     var size = this.$curData.length,
132         i,
133         result;
134 
135    // Called just before iteration
136    this.$before.call(this, this);  
137         
138     for (i = 0; i < size; ++i) {
139       // Called for every item in the Cursor
140       ++this.$index;
141       result = func.call(this, this.get(), this.$index);
142       
143       // Break if a truthy result is returned
144       if (result) {
145         break;
146       } 
147     }
148           
149     // Called just after iteration
150     this.$after.call(this, this);
151   };
152   
153   /**
154    * @class Represents a baja:ICollection in BajaScript.
155    * <p>
156    * Collections are usually returned as the result of resolving an ORD (i.e. a BQL query).
157    *
158    * @see CollectionCursor
159    * 
160    * @name baja.coll.Collection
161    * @extends baja.Simple
162    */  
163   baja.coll.Collection = function (collData) {
164     baja.coll.Collection.$super.apply(this, arguments);
165     this.$collData = collData;
166     this.$Cursor = CollectionCursor;
167   }.$extend(baja.Simple);
168   
169   baja.coll.Collection.DEFAULT = new baja.coll.Collection({});
170     
171   /**
172    * Make a Collection.
173    * 
174    * @private
175    *
176    * @param {Object} collData
177    * @returns {baja.coll.Collection} the Collection
178    */
179   baja.coll.Collection.make = function (collData) {    
180     if (isObjEmpty(collData)) {
181       return baja.coll.Collection.DEFAULT;
182     }
183     return new baja.coll.Collection(collData);
184   };
185   
186   /**
187    * Make a Collection.
188    * 
189    * @private
190    *
191    * @param {Object} collData
192    * @returns {baja.coll.Collection} the Collection
193    */
194   baja.coll.Collection.prototype.make = function (collData) {
195     return baja.coll.Collection.make(collData);
196   };
197   
198   /**
199    * Decode a Collection from a String.
200    * 
201    * @private
202    * 
203    * @param {String} str
204    * @returns {baja.coll.Collection}
205    */
206   baja.coll.Collection.prototype.decodeFromString = function (str) {
207     return baja.coll.Collection.make(JSON.parse(str));
208   };
209   
210   /**
211    * Encode the Collection to a String.
212    *
213    * @private
214    *
215    * @returns {String}
216    */
217   baja.coll.Collection.prototype.encodeToString = function () {
218     return JSON.stringify(this.$collData);
219   };
220     
221   // Register Type  
222   baja.coll.Collection.registerType("box:BoxCollection");
223   
224   /**
225    * Iterate through a Collection.
226    * <p>
227    * Please note, this may retrieve data asynchronously.
228    * <p>
229    * A function is passed in to retrieve to the Cursor.
230    * For example...
231    * 
232    * <pre>
233    *   myCollection.cursor(function (cursor) {
234    *     // Called once we have the Cursor
235    *   });
236    *   
237    *   // or via an Object Literal...
238    *   
239    *   myCollection.cursor({
240    *     each: function () {
241    *       // Called for each item in the Cursor...
242    *       var dataFromCursor = this.get();
243    *     },
244    *     ok: function (cursor) {
245    *       // Called once we have the Cursor
246    *     },
247    *     fail: function (err) {
248    *       // Called if any errors in getting data
249    *     }
250    *   });
251    * </pre>
252    * 
253    * @see CollectionCursor
254    * 
255    * @param {Object|Function} obj the Object Literal that specifies the method's arguments.
256    *                              For convenience, this can also be the ok function.
257    * @param {Function} [obj.ok] called when the cursor has been created with the cursor as an argument.
258    * @param {Function} [obj.fail] called if the cursor fails to be retrieved. 
259    *                              An error is passed in as the first argument.
260    * @param {baja.comm.Batch} [obj.batch], if specified, the operation will be batched into this object.
261    * @param {Function} [obj.before] called just before the Cursor is about to be iterated through.
262    * @param {Function} [obj.after] called just after the Cursor has finished iterating.
263    * @param {Number} [obj.offset] Specifies the row number to start encoding the result-set from.
264    * @param {Number} [obj.limit] Specifies the maximum number of rows that can be encoded. 
265    *                             By default, this is set to 10 rows.
266    */
267   baja.coll.Collection.prototype.cursor = function (obj) {
268     obj = baja.objectify(obj, "each");
269     
270     var cb = new baja.comm.Callback(obj.ok, obj.fail, obj.batch),
271         that = this;
272     
273     // Add an intermediate callback to create the Cursor Object and pass it back
274     cb.addOk(function (ok, fail, resp) {     
275       var newOk = function () {  
276         var i,
277             cursor;
278         
279         // Decode each row of the result set
280         for (i = 0; i < resp.length; ++i) {
281           resp[i] = baja.bson.decodeValue(resp[i], baja.$serverDecodeContext);
282         }
283                 
284         // Create a Cursor Object and pass it into the Handler
285         cursor = new that.$Cursor(that, resp);
286     
287         if (typeof obj.before === "function") {
288           cursor.before(obj.before); 
289         }
290         
291         if (typeof obj.after === "function") {
292           cursor.after(obj.after); 
293         }
294         
295         // Please note, if defined, this will trigger an iteration
296         if (typeof obj.each === "function") {
297           cursor.each(obj.each); 
298         }
299         
300         ok(cursor);
301       };
302         
303       if (!obj.$data) {
304         // Scan for any unknown types we don't have here and make another
305         // network request if necessary      
306         var unknownTypes = baja.bson.scanForUnknownTypes(resp);
307         
308         if (unknownTypes.length > 0) {
309           var importBatch = new baja.comm.Batch();
310           
311           baja.importTypes({
312             "typeSpecs": unknownTypes, 
313             "ok": newOk, 
314             "fail": fail,
315             "batch": importBatch
316           });
317           
318           // Make a synchronous or asynchronous call depending how the orignal callback 
319           // was invoked
320           if (cb.getBatch().isAsync()) {
321             cb.commit();
322           }
323           else {
324             cb.commitSync();
325           }
326         }
327         else {
328           newOk();
329         }
330       }
331       else {
332         newOk();
333       } 
334     });
335     
336     obj.limit = obj.limit || 10;
337     obj.offset = obj.offset || 0;
338     
339     // If '$data' is specified then use this for the first Cursor iteration. This is an optimization made
340     // by BajaScript's ORD architecture. This enables an ORD to be queried and then iterated through at the same time.
341     if (obj.$data) {
342       cb.ok(obj.$data);
343     }
344     else {
345       
346       // Make a a network call for the Cursor data
347       baja.comm.cursor(this.$collData.req, cb, obj);
348     }
349   }; 
350   
351   ////////////////////////////////////////////////////////////////
352   // Collection
353   //////////////////////////////////////////////////////////////// 
354   
355   /**
356    * @class Cursor for a Table.
357    * 
358    * @see baja.coll.Table
359    *
360    * @name TableCursor
361    * @extends CollectionCursor
362    * @inner
363    * @public
364    */
365   var TableCursor = function (collection, curData) {
366     TableCursor.$super.apply(this, arguments);
367     this.$collection = collection;
368   }.$extend(CollectionCursor);
369   
370   function getColDisp(cursor, column, display) { 
371     var tof = typeof column;
372     
373     if (tof === "undefined") {
374       // If no column is defined then just return the entire row
375       return colGet.call(cursor);
376     }
377     else if (column && tof === "object") {
378       column = column.getName();
379     }
380     else if (tof !== "string") {
381       throw new Error("Invalid Column name: " + column);
382     }
383     
384     var row = colGet.call(cursor);
385     if (row !== null) {
386       return display ? row.getDisplay(column) : row.get(column);
387     }
388     
389     return null;
390   }
391         
392   /**
393    * Return the current row or row item. 
394    * <p>
395    * If column information is passed into this method then the value for a particular
396    * column and row will be returned.
397    *
398    * @name TableCursor#get
399    * @function
400    * 
401    * @param {String|TableColumn} [column] the column name or column. If undefined,
402    *                                      the entire row is returned.
403    * @returns the cursor value (null if none available).
404    */
405   TableCursor.prototype.get = function (column) { 
406     return getColDisp(this, column, /*display*/false);
407   }; 
408   
409   /**
410    * Return the current item display string.
411    * <p>
412    * If column information is passed into this method then the display String for a particular
413    * column and row will be returned.
414    * 
415    * @param {String|TableColumn} [column] the column name or column. If undefined,
416    *                                      the entire row is returned.
417    * @returns the cursor display string (null if none available).
418    */
419   TableCursor.prototype.getDisplay = function (column) {
420     return getColDisp(this, column, /*display*/true);
421   }; 
422   
423   /**
424    * @class Represents a baja:ITable in BajaScript.
425    * <p>
426    * Tables are usually returned as the result of resolving an ORD (i.e. a BQL query).
427    *
428    * @name baja.coll.Table
429    * @extends baja.coll.Collection
430    *
431    * @see TableCursor
432    * @see TableColumn
433    */  
434   baja.coll.Table = function (tableData) {
435     baja.coll.Table.$super.apply(this, arguments);
436     this.$tableData = tableData;
437     this.$Cursor = TableCursor;
438   }.$extend(baja.coll.Collection); 
439   
440   baja.coll.Table.DEFAULT = new baja.coll.Table({});
441   
442   /**
443    * Make a Table.
444    * 
445    * @private
446    *
447    * @param {Object} tableData
448    * @returns {baja.coll.Table} the Table.
449    */
450   baja.coll.Table.make = function (tableData) {
451     if (isObjEmpty(tableData)) {
452       return baja.coll.Table.DEFAULT;
453     }
454     return new baja.coll.Table(tableData);
455   };
456   
457   /**
458    * Make a Table.
459    * 
460    * @private
461    *
462    * @param {Object} tableData
463    * @returns {baja.coll.Table} the Table
464    */
465   baja.coll.Table.prototype.make = function (tableData) {
466     return baja.coll.Table.make(tableData);
467   };
468   
469   /**
470    * Decode a Table from a String.
471    * 
472    * @private
473    * 
474    * @param {String} str
475    * @returns {Table}
476    */
477   baja.coll.Table.prototype.decodeFromString = function (str) {
478     return baja.coll.Table.make(JSON.parse(str));
479   };
480   
481   /**
482    * Encode the Table to a String.
483    *
484    * @private
485    *
486    * @returns {String}
487    */
488   baja.coll.Table.prototype.encodeToString = function () {
489     return JSON.stringify(this.$tableData);
490   };
491     
492   // Register Type  
493   baja.coll.Table.registerType("box:BoxTable");
494   
495   /**
496    * Returns an array of Table Columns.
497    *
498    * @see baja.coll.Table#getCol
499    * @see TableColumn
500    *
501    * @returns an array of columns (TableColumn)
502    */
503   baja.coll.Table.prototype.getColumns = function () {
504     var columns = [],
505         cols =  this.$tableData.cols,
506         i;
507     
508     for (i = 0; i < cols.length; ++i) {
509       columns.push(this.getCol(cols[i].n));
510     }
511     
512     return columns;
513   };
514   
515   /**
516    * Returns a Column Object for the given column name.
517    *
518    * @param {String|Number} column the column name or index.
519    * @returns {TableColumn} the table column or null if the column can't be found.
520    */
521   baja.coll.Table.prototype.getCol = function (column) {
522     strictArg(column);
523     
524     var to = typeof column,
525         cols = this.$tableData.cols,
526         data,
527         i;
528     
529     if (to === "number") {
530       data = cols[column];
531     }
532     else if (to === "string") {
533       for (i = 0; i < cols.length; ++i) {
534         if (cols[i].n === column) {
535           data = cols[i]; 
536           break;
537         }
538       }
539     }
540     
541     // If there's no data then return null at this point
542     if (!data) {
543       return null;
544     }
545                   
546     /** 
547      * @class Table Column
548      * @name TableColumn
549      * @inner
550      * @public
551      *
552      * @see baja.coll.Table
553      */
554     return { 
555       /**
556        * Return the column name.
557        * 
558        * @name TableColumn#getName
559        * @returns {String}
560        */
561       getName: function getName() {
562         return data.n;
563       },
564       
565       /**
566        * Return the column display name.
567        * 
568        * @name TableColumn#getDisplayName
569        * @returns {String}
570        */
571       getDisplayName: function getDisplayName() {
572         return data.dn;
573       },
574       
575       /**
576        * Return the column Type.
577        * 
578        * @name TableColumn#getType
579        * @returns {Type}
580        */
581       getType: function getType() {
582         return baja.lt(data.t);
583       },
584       
585       /**
586        * Return the column flags.
587        * 
588        * @name TableColumn#getFlags
589        * @returns {Number}
590        */
591       getFlags: function getFlags() {
592         return data.f;
593       },
594       
595       /**
596        * the column facets
597        * 
598        * @name TableColumn#getFacets
599        * @returns {baja.Facets}
600        */
601       getFacets: function getFacets() {
602         return baja.Facets.DEFAULT.decodeFromString(data.x);
603       }
604     };
605   };
606   
607 }(baja));