]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/hatch.js
c2352944738c086fcfb086240e2476439bd3a10a
[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 = 1;
33     service.messages = {};
34     service.pending = [];
35     service.hatchAvailable = null;
36     service.cachedPrintConfig = {};
37     service.state = 'IDLE'; // IDLE, INIT, CONNECTED, NO_CONNECTION
38
39     // write a message to the Hatch port
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
51         msg2.from = 'page';
52         $window.postMessage(msg2, $window.location.origin);
53     }
54
55     // Send the request to Hatch if it's available.  
56     // Otherwise handle the request locally.
57     service.attemptHatchDelivery = function(msg) {
58
59         msg.msgid = service.msgId++;
60         msg.deferred = $q.defer();
61
62         if (service.state == 'NO_CONNECTION') {
63             msg.deferred.reject(msg);
64
65         } else if (service.state.match(/CONNECTED|INIT/)) {
66             // Hatch is known to be open
67             service.messages[msg.msgid] = msg;
68             service.sendToHatch(msg);
69
70         } else if (service.state == 'IDLE') { 
71             service.messages[msg.msgid] = msg;
72             service.pending.push(msg);
73             $timeout(service.openHatch);
74         }
75
76         return msg.deferred.promise;
77     }
78
79
80     // resolve the promise on the given request and remove
81     // it from our tracked requests.
82     service.resolveRequest = function(msg) {
83
84         if (!service.messages[msg.msgid]) {
85             console.warn('no cached message for ' 
86                 + msg.msgid + ' : ' + JSON.stringify(msg, null, 2));
87             return;
88         }
89
90         // for requests sent through Hatch, only the cached 
91         // request will have the original promise attached
92         msg.deferred = service.messages[msg.msgid].deferred;
93         delete service.messages[msg.msgid]; // un-cache
94
95         switch (service.state) {
96
97             case 'CONNECTED': // received a standard Hatch response
98                 if (msg.status == 200) {
99                     msg.deferred.resolve(msg.content);
100                 } else {
101                     msg.deferred.reject();
102                     console.warn("Hatch command failed with status=" 
103                         + msg.status + " and message=" + msg.message);
104                 }
105                 break;
106
107             case 'INIT':
108                 if (msg.status == 200) {
109                     service.hatchAvailable = true; // public flag
110                     service.state = 'CONNECTED';
111                     service.hatchOpened();
112                 } else {
113                     msg.deferred.reject();
114                     service.hatchWontOpen(msg.message);
115                 }
116                 break;
117
118             default:
119                 console.warn(
120                     "Received message in unexpected state: " + service.state); 
121         }
122     }
123
124     service.openHatch = function() {
125
126         // When the Hatch extension loads, it tacks an attribute onto
127         // the page body to indicate it's available.
128
129         if (!$window.document.body.getAttribute('hatch-is-open')) {
130             service.hatchWontOpen('Hatch is not available');
131             return;
132         }
133
134         $window.addEventListener("message", function(event) {
135             // We only accept messages from our own content script.
136             if (event.source != window) return;
137
138             // We only care about messages from the Hatch extension.
139             if (event.data && event.data.from == 'extension') {
140
141                 console.debug('Hatch says: ' 
142                     + JSON.stringify(event.data, null, 2));
143
144                 service.resolveRequest(event.data);
145             }
146         }); 
147
148         service.state = 'INIT';
149         service.attemptHatchDelivery({action : 'init'});
150     }
151
152     service.hatchWontOpen = function(err) {
153         console.debug("Hatch connection failed: " + err);
154         service.state = 'NO_CONNECTION';
155         service.hatchAvailable = false;
156         service.hatchClosed();
157     }
158
159     service.hatchClosed = function() {
160         service.printers = [];
161         service.printConfig = {};
162         while ( (msg = service.pending.shift()) ) {
163             msg.deferred.reject(msg);
164             delete service.messages[msg.msgid];
165         }
166         if (service.onHatchClose)
167             service.onHatchClose();
168     }
169
170     // Returns true if Hatch is required or if we are currently
171     // communicating with the Hatch service. 
172     service.usingHatch = function() {
173         return service.state == 'CONNECTED' || service.hatchRequired();
174     }
175
176     // Returns true if this browser (via localStorage) is 
177     // configured to require Hatch.
178     service.hatchRequired = function() {
179         return service.getLocalItem('eg.hatch.required');
180     }
181
182     service.hatchOpened = function() {
183         // let others know we're connected
184         if (service.onHatchOpen) service.onHatchOpen();
185
186         // Deliver any previously queued requests 
187         while ( (msg = service.pending.shift()) ) {
188             service.sendToHatch(msg);
189         };
190     }
191
192     service.remotePrint = function(
193         context, contentType, content, withDialog) {
194
195         return service.getPrintConfig(context).then(
196             function(config) {
197                 // print configuration retrieved; print
198                 return service.attemptHatchDelivery({
199                     action : 'print',
200                     settings : config,
201                     content : content, 
202                     contentType : contentType,
203                     showDialog : withDialog,
204                 });
205             }
206         );
207     }
208
209     // 'force' avoids using the config cache
210     service.getPrintConfig = function(context, force) {
211         if (service.cachedPrintConfig[context] && !force) {
212             return $q.when(service.cachedPrintConfig[context])
213         }
214         return service.getRemoteItem('eg.print.config.' + context)
215         .then(function(config) {
216             return service.cachedPrintConfig[context] = config;
217         });
218     }
219
220     service.setPrintConfig = function(context, config) {
221         service.cachedPrintConfig[context] = config;
222         return service.setRemoteItem('eg.print.config.' + context, config);
223     }
224
225     service.getPrinterOptions = function(name) {
226         return service.attemptHatchDelivery({
227             action : 'printer-options',
228             printer : name
229         });
230     }
231
232     service.getPrinters = function() {
233         if (service.printers) // cached printers
234             return $q.when(service.printers);
235
236         return service.attemptHatchDelivery({action : 'printers'}).then(
237
238             // we have remote printers; sort by name and return
239             function(printers) {
240                 service.printers = printers.sort(
241                     function(a,b) {return a.name < b.name ? -1 : 1});
242                 return service.printers;
243             },
244
245             // remote call failed and there is no such thing as local
246             // printers; return empty set.
247             function() { return [] } 
248         );
249     }
250
251     // get the value for a stored item
252     service.getItem = function(key) {
253         return service.getRemoteItem(key)['catch'](
254             function(msg) {
255                 if (service.hatchRequired()) {
256                     console.error("Unable to getItem: " + key
257                      + "; hatchRequired=true, but hatch is not connected");
258                      return null;
259                 }
260                 return service.getLocalItem(msg.key);
261             }
262         );
263     }
264
265     service.getRemoteItem = function(key) {
266         return service.attemptHatchDelivery({
267             key : key,
268             action : 'get'
269         })
270     }
271
272     service.getLocalItem = function(key) {
273         var val = $window.localStorage.getItem(key);
274         if (val == null) return;
275         return JSON.parse(val);
276     }
277
278     service.getLoginSessionItem = function(key) {
279         var val = $cookies.get(key);
280         if (val == null) return;
281         return JSON.parse(val);
282     }
283
284     service.getSessionItem = function(key) {
285         var val = $window.sessionStorage.getItem(key);
286         if (val == null) return;
287         return JSON.parse(val);
288     }
289
290     /**
291      * @param tmp bool Store the value as a session cookie only.  
292      * tmp values are removed during logout or browser close.
293      */
294     service.setItem = function(key, value) {
295         return service.setRemoteItem(key, value)['catch'](
296             function(msg) {
297                 if (service.hatchRequired()) {
298                     console.error("Unable to setItem: " + key
299                      + "; hatchRequired=true, but hatch is not connected");
300                      return null;
301                 }
302                 return service.setLocalItem(msg.key, value);
303             }
304         );
305     }
306
307     // set the value for a stored or new item
308     service.setRemoteItem = function(key, value) {
309         return service.attemptHatchDelivery({
310             key : key, 
311             content : value, 
312             action : 'set',
313         });
314     }
315
316     // Set the value for the given key.
317     // "Local" items persist indefinitely.
318     // If the value is raw, pass it as 'value'.  If it was
319     // externally JSONified, pass it via jsonified.
320     service.setLocalItem = function(key, value, jsonified) {
321         if (jsonified === undefined ) 
322             jsonified = JSON.stringify(value);
323         $window.localStorage.setItem(key, jsonified);
324     }
325
326     // Set the value for the given key.  
327     // "LoginSession" items are removed when the user logs out or the 
328     // browser is closed.
329     // If the value is raw, pass it as 'value'.  If it was
330     // externally JSONified, pass it via jsonified.
331     service.setLoginSessionItem = function(key, value, jsonified) {
332         service.addLoginSessionKey(key);
333         if (jsonified === undefined ) 
334             jsonified = JSON.stringify(value);
335         $cookies.put(key, jsonified);
336     }
337
338     // Set the value for the given key.  
339     // "Session" items are browser tab-specific and are removed when the
340     // tab is closed.
341     // If the value is raw, pass it as 'value'.  If it was
342     // externally JSONified, pass it via jsonified.
343     service.setSessionItem = function(key, value, jsonified) {
344         if (jsonified === undefined ) 
345             jsonified = JSON.stringify(value);
346         $window.sessionStorage.setItem(key, jsonified);
347     }
348
349     // assumes the appender and appendee are both strings
350     // TODO: support arrays as well
351     service.appendLocalItem = function(key, value) {
352         var item = service.getLocalItem(key);
353         if (item) {
354             if (typeof item != 'string') {
355                 logger.warn("egHatch.appendLocalItem => "
356                     + "cannot append to a non-string item: " + key);
357                 return;
358             }
359             value = item + value; // concatenate our value
360         }
361         service.setLocalitem(key, value);
362     }
363
364     // remove a stored item
365     service.removeItem = function(key) {
366         return service.removeRemoteItem(key)['catch'](
367             function(msg) { 
368                 return service.removeLocalItem(msg.key) 
369             }
370         );
371     }
372
373     service.removeRemoteItem = function(key) {
374         return service.attemptHatchDelivery({
375             key : key,
376             action : 'remove'
377         });
378     }
379
380     service.removeLocalItem = function(key) {
381         $window.localStorage.removeItem(key);
382     }
383
384     service.removeLoginSessionItem = function(key) {
385         service.removeLoginSessionKey(key);
386         $cookies.remove(key);
387     }
388
389     service.removeSessionItem = function(key) {
390         $window.sessionStorage.removeItem(key);
391     }
392
393     /**
394      * Remove all "LoginSession" items.
395      */
396     service.clearLoginSessionItems = function() {
397         angular.forEach(service.getLoginSessionKeys(), function(key) {
398             service.removeLoginSessionItem(key);
399         });
400
401         // remove the keys cache.
402         service.removeLocalItem('eg.hatch.login_keys');
403     }
404
405     // if set, prefix limits the return set to keys starting with 'prefix'
406     service.getKeys = function(prefix) {
407         return service.getRemoteKeys(prefix)['catch'](
408             function() { 
409                 if (service.hatchRequired()) {
410                     console.error("Unable to get pref keys; "
411                      + "hatchRequired=true, but hatch is not connected");
412                      return [];
413                 }
414                 return service.getLocalKeys(prefix) 
415             }
416         );
417     }
418
419     service.getRemoteKeys = function(prefix) {
420         return service.attemptHatchDelivery({
421             key : prefix,
422             action : 'keys'
423         });
424     }
425
426     service.getLocalKeys = function(prefix) {
427         var keys = [];
428         var idx = 0;
429         while ( (k = $window.localStorage.key(idx++)) !== null) {
430             // key prefix match test
431             if (prefix && k.substr(0, prefix.length) != prefix) continue; 
432             keys.push(k);
433         }
434         return keys;
435     }
436
437
438     /**
439      * Array of "LoginSession" keys.
440      * Note we have to store these as "Local" items so browser tabs can
441      * share them.  We could store them as cookies, but it's more data
442      * that has to go back/forth to the server.  A "LoginSession" key name is
443      * not private, though, so it's OK if they are left in localStorage
444      * until the next login.
445      */
446     service.getLoginSessionKeys = function(prefix) {
447         var keys = [];
448         var idx = 0;
449         var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
450         angular.forEach(login_keys, function(k) {
451             // key prefix match test
452             if (prefix && k.substr(0, prefix.length) != prefix) return;
453             keys.push(k);
454         });
455         return keys;
456     }
457
458     service.addLoginSessionKey = function(key) {
459         var keys = service.getLoginSessionKeys();
460         if (keys.indexOf(key) < 0) {
461             keys.push(key);
462             service.setLocalItem('eg.hatch.login_keys', keys);
463         }
464     }
465
466     service.removeLoginSessionKey = function(key) {
467         var keys = service.getLoginSessionKeys().filter(function(k) {
468             return k != key;
469         });
470         service.setLocalItem('eg.hatch.login_keys', keys);
471     }
472
473     return service;
474 }])
475