LP#1796945 Reporter cloning and creation fixes
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / idl.js
1 /**
2  * Core Service - egIDL
3  *
4  * IDL parser
5  * usage:
6  *  var aou = new egIDL.aou();
7  *  var fullIDL = egIDL.classes;
8  *
9  *  IDL TODO:
10  *
11  * 1. selector field only appears once per class.  We could save
12  *    a lot of IDL (network) space storing it only once at the 
13  *    class level.
14  * 2. we don't need to store array_position in /IDL2js since it
15  *    can be derived at parse time.  Ditto saving space.
16  */
17 angular.module('egCoreMod')
18
19 .factory('egIDL', ['$window', function($window) {
20
21     var service = {};
22
23     // Clones data structures containing fieldmapper objects
24     service.Clone = function(old, depth) {
25         if (depth === undefined) depth = 100;
26         var obj;
27         if (typeof old == 'undefined' || old === null) {
28             return old;
29         } else if (old._isfieldmapper) {
30             obj = new service[old.classname]()
31             if (old.a) obj.a = service.Clone(old.a, depth); // pass same depth because we're still cloning this same object
32         } else {
33             if(angular.isArray(old)) {
34                 obj = [];
35             } else if(angular.isObject(old)) {
36                 obj = {};
37             } else {
38                  return angular.copy(old);
39             }
40
41             for( var j in old ) {
42                 if (old[j] === null || typeof old[j] == 'undefined') {
43                     obj[j] = old[j];
44                 } else if( old[j]._isfieldmapper ) {
45                     if (depth) obj[j] = service.Clone(old[j], depth - 1);
46                 } else {
47                     obj[j] = angular.copy(old[j]);
48                 }
49             }
50         }
51         return obj;
52     };
53
54     service.parseIDL = function() {
55         //console.debug('egIDL.parseIDL()');
56
57         // retain a copy of the full IDL within the service
58         service.classes = $window._preload_fieldmapper_IDL;
59
60         // keep this reference around (note: not a clone, just a ref)
61         // so that unit tests, which repeatedly instantiate the
62         // service will work.
63         //$window._preload_fieldmapper_IDL = null;
64
65         /**
66          * Creates the class constructor and getter/setter
67          * methods for each IDL class.
68          */
69         function mkclass(cls, fields) {
70
71             service.classes[cls].core_label = service.classes[cls].core ? 'Core sources' : 'Non-core sources';
72             service.classes[cls].classname = cls;
73
74             service[cls] = function(seed) {
75                 this.a = seed || [];
76                 this.classname = cls;
77                 this._isfieldmapper = true;
78             }
79
80             /** creates the getter/setter methods for each field */
81             angular.forEach(fields, function(field, idx) {
82                 service[cls].prototype[fields[idx].name] = function(n) {
83                     if (arguments.length==1) this.a[idx] = n;
84                     return this.a[idx];
85                 }
86             });
87
88             // global class constructors required for JSON_v1.js
89             $window[cls] = service[cls]; 
90         }
91
92         for (var cls in service.classes) 
93             mkclass(cls, service.classes[cls].fields);
94     };
95
96     /**
97      * Generate a hash version of an IDL object.
98      *
99      * Flatten determines if nested objects should be squashed into
100      * the top-level hash.
101      *
102      * If 'flatten' is false, e.g.:
103      *
104      * {"call_number" : {"label" :  "foo"}}
105      *
106      * If 'flatten' is true, e.g.:
107      *
108      * e.g.  {"call_number.label" : "foo"}
109      */
110     service.toHash = function(obj, flatten) {
111         if (!angular.isObject(obj)) return obj; // arrays are objects
112
113         if (angular.isArray(obj)) { // NOTE: flatten arrays not supported
114             return obj.map(function(item) {return service.toHash(item)});
115         }
116
117         var field_names = obj.classname ? 
118             Object.keys(service.classes[obj.classname].field_map) :
119             Object.keys(obj);
120
121         var hash = {};
122         angular.forEach(
123             field_names,
124             function(field) { 
125
126                 var val = service.toHash(
127                     angular.isFunction(obj[field]) ? 
128                         obj[field]() : obj[field], 
129                     flatten
130                 );
131
132                 if (flatten && angular.isObject(val)) {
133                     angular.forEach(val, function(sub_val, key) {
134                         var fname = field + '.' + key;
135                         hash[fname] = sub_val;
136                     });
137
138                 } else if (val !== undefined) {
139                     hash[field] = val;
140                 }
141             }
142         );
143
144         return hash;
145     }
146
147     service.toTypedHash = function(obj) {
148         if (!angular.isObject(obj)) return obj; // arrays are objects
149
150         if (angular.isArray(obj)) { // NOTE: flatten arrays not supported
151             return obj.map(function(item) {return service.toTypedHash(item)});
152         }
153
154         var field_names = obj.classname ? 
155             Object.keys(service.classes[obj.classname].field_map) :
156             Object.keys(obj);
157
158         var hash = {};
159         if (obj.classname) {
160             angular.extend(hash, {
161                 _classname : obj.classname
162             });
163         }
164         angular.forEach(
165             field_names,
166             function(field) { 
167
168                 var val = service.toTypedHash(
169                     angular.isFunction(obj[field]) ? 
170                         obj[field]() : obj[field]
171                 );
172
173                 if (val !== undefined) {
174                     if (obj.classname) {
175                         switch(service.classes[obj.classname].field_map[field].datatype) {
176                             case 'org_unit' :
177                                 // aou fieldmapper objects get used as is because
178                                 // that's what egOrgSelector expects
179                                 // TODO we should probably make egOrgSelector more flexible
180                                 //      in what it can bind to
181                                 hash[field] = obj[field]();
182                                 break;
183                             case 'timestamp':
184                                 hash[field] = (val === null) ? val : new Date(val);
185                                 break;
186                             case 'bool':
187                                 if (val == 't') {
188                                     hash[field] = true;
189                                 } else if (val == 'f') {
190                                     hash[field] = false;
191                                 } else {
192                                     hash[field] = null;
193                                 }
194                                 break;
195                             default:
196                                 hash[field] = val;
197                         }
198                     } else {
199                         hash[field] = val;
200                     }
201                 }
202             }
203         );
204
205         return hash;
206     }
207
208     // returns a simple string key=value string of an IDL object.
209     service.toString = function(obj) {
210         var s = '';
211         angular.forEach(
212             service.classes[obj.classname].fields.sort(
213                 function(a,b) {return a.name < b.name ? -1 : 1}),
214             function(field) {
215                 s += field.name + '=' + obj[field.name]() + '\n';
216             }
217         );
218         return s;
219     }
220
221     // hash-to-IDL object translater.  Does not support nested values.
222     service.fromHash = function(cls, hash) {
223         if (!service.classes[cls]) {
224             console.error('No such IDL class ' + cls);
225             return null;
226         }
227
228         var new_obj = new service[cls]();
229         angular.forEach(hash, function(val, key) {
230             if (!angular.isFunction(new_obj[key])) return;
231             new_obj[key](hash[key]);
232         });
233
234         return new_obj;
235     }
236
237     service.fromTypedHash = function(hash) {
238         if (!angular.isObject(hash)) return hash;
239         if (angular.isArray(hash)) {
240             return hash.map(function(item) {return service.fromTypedHash(item)});
241         }
242         if (!hash._classname) return;
243
244         var new_obj = new service[hash._classname];
245         var fields = service.classes[hash._classname].field_map;
246         angular.forEach(fields, function(field) {
247             switch(field.datatype) {
248                 case 'org_unit':
249                     if (angular.isFunction(hash[field.name])) {
250                         new_obj[field.name] = hash[field.name];
251                     } else {
252                         new_obj[field.name](hash[field.name]);
253                     }
254                     break;
255                 case 'timestamp':
256                     if (hash[field.name] instanceof Date) {
257                         new_obj[field.name](hash[field.name].toISOString());
258                     }
259                     break;
260                 case 'bool':
261                     if (hash[field.name] === true) {
262                         new_obj[field.name]('t');
263                     } else if (hash[field.name] === false) {
264                         new_obj[field.name]('f');
265                     }
266                     break;
267                 default:
268                     new_obj[field.name](service.fromTypedHash(hash[field.name]));
269             }
270         });
271         new_obj.isnew(hash._isnew);
272         new_obj.ischanged(hash._ischanged);
273         new_obj.isdeleted(hash._isdeleted);
274         return new_obj;
275     }
276
277     // Transforms a flattened hash (see toHash() or egGridFlatDataProvider)
278     // to a nested hash.
279     //
280     // e.g. {"call_number.label" : "foo"} => {"call_number":{"label":"foo"}}
281     service.flatToNestedHash = function(obj) {
282         var hash = {};
283         angular.forEach(obj, function(val, key) {
284             var parts = key.split('.');
285             var sub_hash = hash;
286             var last_key;
287             for (var i = 0; i < parts.length; i++) {
288                 var part = parts[i];
289                 if (i == parts.length - 1) {
290                     sub_hash[part] = val;
291                     break;
292                 } else {
293                     if (!sub_hash[part])
294                         sub_hash[part] = {};
295                     sub_hash = sub_hash[part];
296                 }
297             }
298         });
299
300         return hash;
301     }
302
303     // Using IDL links, allow construction of a tree-ish data structure from
304     // the IDL2js web service output.  This structure will be directly usable
305     // by the <treecontrol> directive
306     service.classTree = {
307         top : null
308     };
309
310     function _sort_class_fields (a,b) {
311         var aname = a.label || a.name;
312         var bname = b.label || b.name;
313         return aname > bname ? 1 : -1;
314     }
315
316     service.classTree.buildNode = function (cls, args) {
317         if (!cls) return null;
318
319         var n = service.classes[cls];
320         if (!n) return null;
321
322         if (!args)
323             args = { label : n.label };
324
325         args.id = cls;
326         if (args.from)
327             args.id = args.from + '.' + args.id;
328
329         return angular.extend( args, {
330             idl     : service[cls],
331             uplink  : args.link,
332             classname: cls,
333             struct  : n,
334             table   : n.table,
335             fields  : n.fields.sort( _sort_class_fields ),
336             links   : n.fields
337                 .filter( function(x) { return x.type == 'link'; } )
338                 .sort( _sort_class_fields ),
339             children: []
340         });
341     }
342
343     service.classTree.fleshNode = function ( node ) {
344         if (node.children.length > 0)
345             return node; // already done already
346
347         angular.forEach(
348             node.links.sort( _sort_class_fields ),
349             function (n) {
350                 var nlabel = n.label ? n.label : n.name;
351                 node.children.push(
352                     service.classTree.buildNode(
353                         n["class"],
354                         {   label : nlabel,
355                             from  : node.id,
356                             link  : n
357                         }
358                     )
359                 );
360             }
361         );
362
363         return node;
364     }
365
366     service.classTree.setTop = function (cls) {
367         console.debug('setTop: '+cls);
368         return service.classTree.top = service.classTree.fleshNode(
369             service.classTree.buildNode(cls)
370         );
371     }
372
373     service.classTree.getTop = function () {
374         return service.classTree.top;
375     }
376
377     return service;
378 }])
379 ;