1b47ec1443ee9abc936067babfe4416dc7306cb8
[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) {
25         var obj;
26         if (typeof old == 'undefined') {
27             return old;
28         } else if (old._isfieldmapper) {
29             obj = new service[old.classname]()
30
31             for( var i in old.a ) {
32                 var thing = old.a[i];
33                 if(thing === null) continue;
34
35                 if (typeof thing == 'undefined') {
36                     obj.a[i] = thing;
37                 } else if (thing._isfieldmapper) {
38                     obj.a[i] = service.Clone(thing);
39                 } else {
40
41                     if(angular.isArray(thing)) {
42                         obj.a[i] = [];
43
44                         for( var j in thing ) {
45
46                             if (typeof thing[j] == 'undefined')
47                                 obj.a[i][j] = thing[j];
48                             else if( thing[j]._isfieldmapper )
49                                 obj.a[i][j] = service.Clone(thing[j]);
50                             else
51                                 obj.a[i][j] = angular.copy(thing[j]);
52                         }
53                     }
54                 }
55             }
56         } else {
57             if(angular.isArray(old)) {
58                 obj = [];
59                 for( var j in old ) {
60                     if (typeof old[j] == 'undefined')
61                         obj[j] = old[j];
62                     else if( old[j]._isfieldmapper )
63                         obj[j] = service.Clone(old[j]);
64                     else
65                         obj[j] = angular.copy(old[j]);
66                 }
67             } else if(angular.isObject(old)) {
68                 obj = {};
69                 for( var j in old ) {
70                     if (typeof old[j] == 'undefined')
71                         obj[j] = old[j];
72                     else if( old[j]._isfieldmapper )
73                         obj[j] = service.Clone(old[j]);
74                     else
75                         obj[j] = angular.copy(old[j]);
76                 }
77             } else {
78                 obj = angular.copy(old);
79             }
80         }
81         return obj;
82     };
83
84     service.parseIDL = function() {
85         //console.debug('egIDL.parseIDL()');
86
87         // retain a copy of the full IDL within the service
88         service.classes = $window._preload_fieldmapper_IDL;
89
90         // keep this reference around (note: not a clone, just a ref)
91         // so that unit tests, which repeatedly instantiate the
92         // service will work.
93         //$window._preload_fieldmapper_IDL = null;
94
95         /**
96          * Creates the class constructor and getter/setter
97          * methods for each IDL class.
98          */
99         function mkclass(cls, fields) {
100
101             service.classes[cls].core_label = service.classes[cls].core ? 'Core sources' : 'Non-core sources';
102             service.classes[cls].classname = cls;
103
104             service[cls] = function(seed) {
105                 this.a = seed || [];
106                 this.classname = cls;
107                 this._isfieldmapper = true;
108             }
109
110             /** creates the getter/setter methods for each field */
111             angular.forEach(fields, function(field, idx) {
112                 service[cls].prototype[fields[idx].name] = function(n) {
113                     if (arguments.length==1) this.a[idx] = n;
114                     return this.a[idx];
115                 }
116             });
117
118             // global class constructors required for JSON_v1.js
119             $window[cls] = service[cls]; 
120         }
121
122         for (var cls in service.classes) 
123             mkclass(cls, service.classes[cls].fields);
124     };
125
126     /**
127      * Generate a hash version of an IDL object.
128      *
129      * Flatten determines if nested objects should be squashed into
130      * the top-level hash.
131      *
132      * If 'flatten' is false, e.g.:
133      *
134      * {"call_number" : {"label" :  "foo"}}
135      *
136      * If 'flatten' is true, e.g.:
137      *
138      * e.g.  {"call_number.label" : "foo"}
139      */
140     service.toHash = function(obj, flatten) {
141         if (!angular.isObject(obj)) return obj; // arrays are objects
142
143         if (angular.isArray(obj)) { // NOTE: flatten arrays not supported
144             return obj.map(function(item) {return service.toHash(item)});
145         }
146
147         var field_names = obj.classname ? 
148             Object.keys(service.classes[obj.classname].field_map) :
149             Object.keys(obj);
150
151         var hash = {};
152         angular.forEach(
153             field_names,
154             function(field) { 
155
156                 var val = service.toHash(
157                     angular.isFunction(obj[field]) ? 
158                         obj[field]() : obj[field], 
159                     flatten
160                 );
161
162                 if (flatten && angular.isObject(val)) {
163                     angular.forEach(val, function(sub_val, key) {
164                         var fname = field + '.' + key;
165                         hash[fname] = sub_val;
166                     });
167
168                 } else if (val !== undefined) {
169                     hash[field] = val;
170                 }
171             }
172         );
173
174         return hash;
175     }
176
177     // returns a simple string key=value string of an IDL object.
178     service.toString = function(obj) {
179         var s = '';
180         angular.forEach(
181             service.classes[obj.classname].fields.sort(
182                 function(a,b) {return a.name < b.name ? -1 : 1}),
183             function(field) {
184                 s += field.name + '=' + obj[field.name]() + '\n';
185             }
186         );
187         return s;
188     }
189
190     // hash-to-IDL object translater.  Does not support nested values.
191     service.fromHash = function(cls, hash) {
192         if (!service.classes[cls]) {
193             console.error('No such IDL class ' + cls);
194             return null;
195         }
196
197         var new_obj = new service[cls]();
198         angular.forEach(hash, function(val, key) {
199             if (!angular.isFunction(new_obj[key])) return;
200             new_obj[key](hash[key]);
201         });
202
203         return new_obj;
204     }
205
206     // Transforms a flattened hash (see toHash() or egGridFlatDataProvider)
207     // to a nested hash.
208     //
209     // e.g. {"call_number.label" : "foo"} => {"call_number":{"label":"foo"}}
210     service.flatToNestedHash = function(obj) {
211         var hash = {};
212         angular.forEach(obj, function(val, key) {
213             var parts = key.split('.');
214             var sub_hash = hash;
215             var last_key;
216             for (var i = 0; i < parts.length; i++) {
217                 var part = parts[i];
218                 if (i == parts.length - 1) {
219                     sub_hash[part] = val;
220                     break;
221                 } else {
222                     if (!sub_hash[part])
223                         sub_hash[part] = {};
224                     sub_hash = sub_hash[part];
225                 }
226             }
227         });
228
229         return hash;
230     }
231
232     // Using IDL links, allow construction of a tree-ish data structure from
233     // the IDL2js web service output.  This structure will be directly usable
234     // by the <treecontrol> directive
235     service.classTree = {
236         top : null
237     };
238
239     function _sort_class_fields (a,b) {
240         var aname = a.label || a.name;
241         var bname = b.label || b.name;
242         return aname > bname ? 1 : -1;
243     }
244
245     service.classTree.buildNode = function (cls, args) {
246         if (!cls) return null;
247
248         var n = service.classes[cls];
249         if (!n) return null;
250
251         if (!args)
252             args = { label : n.label };
253
254         args.id = cls;
255         if (args.from)
256             args.id = args.from + '.' + args.id;
257
258         return angular.extend( args, {
259             idl     : service[cls],
260             jtype   : 'inner',
261             uplink  : args.link,
262             classname: cls,
263             struct  : n,
264             table   : n.table,
265             fields  : n.fields.sort( _sort_class_fields ),
266             links   : n.fields
267                 .filter( function(x) { return x.type == 'link'; } )
268                 .sort( _sort_class_fields ),
269             children: []
270         });
271     }
272
273     service.classTree.fleshNode = function ( node ) {
274         if (node.children.length > 0)
275             return node; // already done already
276
277         angular.forEach(
278             node.links.sort( _sort_class_fields ),
279             function (n) {
280                 var nlabel = n.label ? n.label : n.name;
281                 node.children.push(
282                     service.classTree.buildNode(
283                         n["class"],
284                         {   label : nlabel,
285                             from  : node.id,
286                             link  : n
287                         }
288                     )
289                 );
290             }
291         );
292
293         return node;
294     }
295
296     service.classTree.setTop = function (cls) {
297         console.debug('setTop: '+cls);
298         return service.classTree.top = service.classTree.fleshNode(
299             service.classTree.buildNode(cls)
300         );
301     }
302
303     service.classTree.getTop = function () {
304         return service.classTree.top;
305     }
306
307     return service;
308 }])
309 ;