]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/hatch.js
Apply sort for ident_type dropdown
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / hatch.js
1 /**
2  * Core Service - egHatch
3  *
4  * Dispatches print and data storage requests to the appropriate handler.
5  *
6  * With each top-level request, if a connection to Hatch is established,
7  * the request is relayed.  If a connection has not been attempted, an
8  * attempt is made then the request is handled.  If Hatch is known to be
9  * inaccessible, requests are routed to local handlers.
10  *
11  * Most handlers also provide direct remote and local variants to the
12  * application can decide to which to use as needed.
13  *
14  * Local storage requests are handled by $window.localStorage.
15  *
16  * Note that all top-level and remote requests return promises.  All
17  * local requests return immediate values, since local requests are
18  * never asynchronous.
19  *
20  * BEWARE: never store "fieldmapper" objects, since their structure
21  * may change over time as the IDL changes.  Always flatten objects
22  * into key/value pairs before calling set*Item()
23  *
24  */
25 angular.module('egCoreMod')
26
27 .factory('egHatch',
28            ['$q','$window','$timeout','$interpolate','$http','$cookies',
29     function($q , $window , $timeout , $interpolate , $http , $cookies) {
30
31     var service = {};
32     service.msgId = 0;
33     service.messages = {};
34     service.pending = [];
35     service.socket = null;
36     service.hatchAvailable = null;
37     service.defaultHatchURL = 'wss://localhost:8443/hatch'; 
38
39     // write a message to the Hatch websocket
40     service.sendToHatch = function(msg) {
41         var msg2 = {};
42
43         // shallow copy and scrub msg before sending
44         angular.forEach(msg, function(val, key) {
45             if (key.match(/deferred/)) return;
46             msg2[key] = val;
47         });
48
49         console.debug("sending to Hatch: " + JSON.stringify(msg2,null,2));
50         service.socket.send(JSON.stringify(msg2));
51     }
52
53     // Send the request to Hatch if it's available.  
54     // Otherwise handle the request locally.
55     service.attemptHatchDelivery = function(msg) {
56
57         msg.msgid = service.msgId++;
58         msg.deferred = $q.defer();
59
60         if (service.hatchAvailable === false) { // Hatch is closed
61             msg.deferred.reject(msg);
62
63         } else if (service.hatchAvailable === true) { // Hatch is open
64             // Hatch is known to be open
65             service.messages[msg.msgid] = msg;
66             service.sendToHatch(msg);
67
68         } else {  // Hatch status unknown; attempt to connect
69             service.messages[msg.msgid] = msg;
70             service.pending.push(msg);
71             service.hatchConnect();
72         }
73
74         return msg.deferred.promise;
75     }
76
77
78     // resolve the promise on the given request and remove
79     // it from our tracked requests.
80     service.resolveRequest = function(msg) {
81
82         if (!service.messages[msg.msgid]) {
83             console.warn('no cached message for ' 
84                 + msg.msgid + ' : ' + JSON.stringify(msg, null, 2));
85             return;
86         }
87
88         // for requests sent through Hatch, only the cached 
89         // request will have the original promise attached
90         msg.deferred = service.messages[msg.msgid].deferred;
91         delete service.messages[msg.msgid]; // un-cache
92
93         // resolve / reject
94         if (msg.error) {
95             throw new Error(
96             "egHatch command failed : " 
97                 + JSON.stringify(msg.error, null, 2));
98         } else {
99             msg.deferred.resolve(msg.content);
100         } 
101     }
102
103     service.hatchClosed = function() {
104         service.socket = null;
105         service.printers = [];
106         service.printConfig = {};
107         while ( (msg = service.pending.shift()) ) {
108             msg.deferred.reject(msg);
109             delete service.messages[msg.msgid];
110         }
111         if (service.onHatchClose)
112             service.onHatchClose();
113     }
114
115     service.hatchURL = function() {
116         return service.getLocalItem('eg.hatch.url') 
117             || service.defaultHatchURL;
118     }
119
120     // Returns true if Hatch is required or if we are currently
121     // communicating with the Hatch service. 
122     service.usingHatch = function() {
123         return service.hatchAvailable || service.hatchRequired();
124     }
125
126     // Returns true if this browser (via localStorage) is 
127     // configured to require Hatch.
128     service.hatchRequired = function() {
129         return service.getLocalItem('eg.hatch.required');
130     }
131
132     service.hatchConnect = function() {
133
134         if (service.socket && 
135             service.socket.readyState == service.socket.CONNECTING) {
136             // connection in progress.  Nothing to do.  Our queued
137             // message will be delivered when onopen() fires
138             return;
139         }
140
141         try {
142             service.socket = new WebSocket(service.hatchURL());
143         } catch(e) {
144             service.hatchAvailable = false;
145             service.hatchClosed();
146             return;
147         }
148
149         service.socket.onopen = function() {
150             console.debug('connected to Hatch');
151             service.hatchAvailable = true;
152             if (service.onHatchOpen) 
153                 service.onHatchOpen();
154             while ( (msg = service.pending.shift()) ) {
155                 service.sendToHatch(msg);
156             };
157         }
158
159         service.socket.onclose = function() {
160             if (service.hatchAvailable === false) return; // already registered
161
162             // onclose() will be called regularly as we disconnect from
163             // Hatch via timeouts.  Return hatchAvailable to its unknow state
164             service.hatchAvailable = null;
165             service.hatchClosed();
166         }
167
168         service.socket.onerror = function() {
169             if (service.hatchAvailable === false) return; // already registered
170             service.hatchAvailable = false;
171             console.debug(
172                 "unable to connect to Hatch server at " + service.hatchURL());
173             service.hatchClosed();
174         }
175
176         service.socket.onmessage = function(evt) {
177             var msgStr = evt.data;
178             if (!msgStr) throw new Error("Hatch returned empty message");
179
180             var msgObj = JSON.parse(msgStr);
181             console.debug('Hatch says ' + JSON.stringify(msgObj, null, 2));
182             service.resolveRequest(msgObj); 
183         }
184     }
185
186     service.getPrintConfig = function() {
187         if (service.printConfig) 
188             return $q.when(service.printConfig);
189
190         return service.getRemoteItem('eg.print.config')
191         .then(function(conf) { 
192             return (service.printConfig = conf || {}) 
193         });
194     }
195
196     service.setPrintConfig = function(conf) {
197         service.printConfig = conf;
198         return service.setRemoteItem('eg.print.config', conf);
199     }
200
201
202     service.remotePrint = function(
203         context, contentType, content, withDialog) {
204
205         return service.getPrintConfig().then(
206             function(conf) {
207                 // print configuration retrieved; print
208                 return service.attemptHatchDelivery({
209                     action : 'print',
210                     config : conf[context],
211                     content : content, 
212                     contentType : contentType,
213                     showDialog : withDialog,
214                 });
215             }
216         );
217     }
218
219     // launch the print dialog then attach the resulting configuration
220     // to the requested context, then store the final values.
221     service.configurePrinter = function(context, printer) {
222
223         // load current settings
224         return service.getPrintConfig()
225
226         // dispatch the print configuration request
227         .then(function(config) {
228
229             // loaded remote config
230             if (!config[context]) config[context] = {};
231             config[context].printer = printer;
232             return service.attemptHatchDelivery({
233                 key : 'no-op', 
234                 action : 'print-config',
235                 config : config[context]
236             })
237         })
238
239         // set the returned settings to the requested context
240         .then(function(newconf) {
241             if (angular.isObject(newconf)) {
242                 newconf.printer = printer;
243                 return service.printConfig[context] = newconf;
244             } else {
245                 console.warn("configurePrinter() returned " + newconf);
246             }
247         })
248
249         // store the newly linked settings
250         .then(function() {
251             service.setItem('eg.print.config', service.printConfig);
252         })
253
254         // return the final settings to the caller
255         .then(function() {return service.printConfig});
256     }
257
258     service.getPrinters = function() {
259         if (service.printers) // cached printers
260             return $q.when(service.printers);
261
262         return service.attemptHatchDelivery({action : 'printers'}).then(
263
264             // we have remote printers; sort by name and return
265             function(printers) {
266                 service.printers = printers.sort(
267                     function(a,b) {return a.name < b.name ? -1 : 1});
268                 return service.printers;
269             },
270
271             // remote call failed and there is no such thing as local
272             // printers; return empty set.
273             function() { return [] } 
274         );
275     }
276
277     // get the value for a stored item
278     service.getItem = function(key) {
279         return service.getRemoteItem(key)['catch'](
280             function(msg) {
281                 if (service.hatchRequired()) {
282                     console.error("Unable to getItem: " + key
283                      + "; hatchRequired=true, but hatch is not connected");
284                      return null;
285                 }
286                 return service.getLocalItem(msg.key);
287             }
288         );
289     }
290
291     service.getRemoteItem = function(key) {
292         return service.attemptHatchDelivery({
293             key : key,
294             action : 'get', 
295         });
296     }
297
298     service.getLocalItem = function(key) {
299         var val = $window.localStorage.getItem(key);
300         if (val == null) return;
301         return JSON.parse(val);
302     }
303
304     service.getLoginSessionItem = function(key) {
305         var val = $cookies.get(key);
306         if (val == null) return;
307         return JSON.parse(val);
308     }
309
310     service.getSessionItem = function(key) {
311         var val = $window.sessionStorage.getItem(key);
312         if (val == null) return;
313         return JSON.parse(val);
314     }
315
316     /**
317      * @param tmp bool Store the value as a session cookie only.  
318      * tmp values are removed during logout or browser close.
319      */
320     service.setItem = function(key, value) {
321         var str = JSON.stringify(value);
322
323         return service.setRemoteItem(key, str)['catch'](
324             function(msg) {
325                 if (service.hatchRequired()) {
326                     console.error("Unable to setItem: " + key
327                      + "; hatchRequired=true, but hatch is not connected");
328                      return null;
329                 }
330                 return service.setLocalItem(msg.key, null, str);
331             }
332         );
333     }
334
335     // set the value for a stored or new item
336     service.setRemoteItem = function(key, value) {
337         return service.attemptHatchDelivery({
338             key : key, 
339             value : value, 
340             action : 'set',
341         });
342     }
343
344     // Set the value for the given key.
345     // "Local" items persist indefinitely.
346     // If the value is raw, pass it as 'value'.  If it was
347     // externally JSONified, pass it via jsonified.
348     service.setLocalItem = function(key, value, jsonified) {
349         if (jsonified === undefined ) 
350             jsonified = JSON.stringify(value);
351         $window.localStorage.setItem(key, jsonified);
352     }
353
354     // Set the value for the given key.  
355     // "LoginSession" items are removed when the user logs out or the 
356     // browser is closed.
357     // If the value is raw, pass it as 'value'.  If it was
358     // externally JSONified, pass it via jsonified.
359     service.setLoginSessionItem = function(key, value, jsonified) {
360         service.addLoginSessionKey(key);
361         if (jsonified === undefined ) 
362             jsonified = JSON.stringify(value);
363         $cookies.put(key, jsonified);
364     }
365
366     // Set the value for the given key.  
367     // "Session" items are browser tab-specific and are removed when the
368     // tab is closed.
369     // If the value is raw, pass it as 'value'.  If it was
370     // externally JSONified, pass it via jsonified.
371     service.setSessionItem = function(key, value, jsonified) {
372         if (jsonified === undefined ) 
373             jsonified = JSON.stringify(value);
374         $window.sessionStorage.setItem(key, jsonified);
375     }
376
377     // appends the value to the existing item stored at key.
378     // If not item is found at key, this behaves just like setItem()
379     service.appendItem = function(key, value) {
380         return service.appendRemoteItem(key, value)['catch'](
381             function(msg) {
382                 if (service.hatchRequired()) {
383                     console.error("Unable to appendItem: " + key
384                      + "; hatchRequired=true, but hatch is not connected");
385                      return null;
386                 }
387                 service.appendLocalItem(msg.key, msg.value);
388             }
389         );
390     }
391
392     service.appendRemoteItem = function(key, value) {
393         return service.attemptHatchDelivery({
394             key : key, 
395             value : value, 
396             action : 'append',
397         });
398     }
399
400     // assumes the appender and appendee are both strings
401     // TODO: support arrays as well
402     service.appendLocalItem = function(key, value) {
403         var item = service.getLocalItem(key);
404         if (item) {
405             if (typeof item != 'string') {
406                 logger.warn("egHatch.appendLocalItem => "
407                     + "cannot append to a non-string item: " + key);
408                 return;
409             }
410             value = item + value; // concatenate our value
411         }
412         service.setLocalitem(key, value);
413     }
414
415     // remove a stored item
416     service.removeItem = function(key) {
417         return service.removeRemoteItem(key)['catch'](
418             function(msg) { 
419                 return service.removeLocalItem(msg.key) 
420             }
421         );
422     }
423
424     service.removeRemoteItem = function(key) {
425         return service.attemptHatchDelivery({
426             key : key,
427             action : 'remove'
428         });
429     }
430
431     service.removeLocalItem = function(key) {
432         $window.localStorage.removeItem(key);
433     }
434
435     service.removeLoginSessionItem = function(key) {
436         service.removeLoginSessionKey(key);
437         $cookies.remove(key);
438     }
439
440     service.removeSessionItem = function(key) {
441         $window.sessionStorage.removeItem(key);
442     }
443
444     /**
445      * Remove all "LoginSession" items.
446      */
447     service.clearLoginSessionItems = function() {
448         angular.forEach(service.getLoginSessionKeys(), function(key) {
449             service.removeLoginSessionItem(key);
450         });
451
452         // remove the keys cache.
453         service.removeLocalItem('eg.hatch.login_keys');
454     }
455
456     // if set, prefix limits the return set to keys starting with 'prefix'
457     service.getKeys = function(prefix) {
458         return service.getRemoteKeys(prefix)['catch'](
459             function() { 
460                 if (service.hatchRequired()) {
461                     console.error("Unable to get pref keys; "
462                      + "hatchRequired=true, but hatch is not connected");
463                      return [];
464                 }
465                 return service.getLocalKeys(prefix) 
466             }
467         );
468     }
469
470     service.getRemoteKeys = function(prefix) {
471         return service.attemptHatchDelivery({
472             key : prefix,
473             action : 'keys'
474         });
475     }
476
477     service.getLocalKeys = function(prefix) {
478         var keys = [];
479         var idx = 0;
480         while ( (k = $window.localStorage.key(idx++)) !== null) {
481             // key prefix match test
482             if (prefix && k.substr(0, prefix.length) != prefix) continue; 
483             keys.push(k);
484         }
485         return keys;
486     }
487
488
489     /**
490      * Array of "LoginSession" keys.
491      * Note we have to store these as "Local" items so browser tabs can
492      * share them.  We could store them as cookies, but it's more data
493      * that has to go back/forth to the server.  A "LoginSession" key name is
494      * not private, though, so it's OK if they are left in localStorage
495      * until the next login.
496      */
497     service.getLoginSessionKeys = function(prefix) {
498         var keys = [];
499         var idx = 0;
500         var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
501         angular.forEach(login_keys, function(k) {
502             // key prefix match test
503             if (prefix && k.substr(0, prefix.length) != prefix) return;
504             keys.push(k);
505         });
506         return keys;
507     }
508
509     service.addLoginSessionKey = function(key) {
510         var keys = service.getLoginSessionKeys();
511         if (keys.indexOf(key) < 0) {
512             keys.push(key);
513             service.setLocalItem('eg.hatch.login_keys', keys);
514         }
515     }
516
517     service.removeLoginSessionKey = function(key) {
518         var keys = service.getLoginSessionKeys().filter(function(k) {
519             return k != key;
520         });
521         service.setLocalItem('eg.hatch.login_keys', keys);
522     }
523
524     return service;
525 }])
526