]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/hatch.js
LP1615805 No inputs after submit in patron search (AngularJS)
[working/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  * If Hatch is configured to honor the request -- current request types
7  * are 'settings', 'offline', and 'printing' -- the request will be
8  * relayed to the Hatch service.  Otherwise, the request is handled
9  * locally.
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','$cookies','egNet','$injector',
29     function($q , $window , $timeout , $interpolate , $cookies , egNet , $injector ) {
30
31     var service = {};
32     service.msgId = 1;
33     service.messages = {};
34     service.hatchAvailable = false;
35     service.auth = null;  // ref to egAuth loaded on-demand to avoid circular ref.
36     service.disableServerSettings = false;
37
38     // key/value cache -- avoid unnecessary Hatch extension requests.
39     // Only affects *RemoteItem calls.
40     service.keyCache = {}; 
41
42     // Keep a local copy of all retrieved setting summaries, which indicate
43     // which setting types exist for each setting.  
44     service.serverSettingSummaries = {};
45
46     /**
47      * Settings with these prefixes will always live in the browser.
48      */
49     service.browserOnlyPrefixes = [
50         'eg.hatch.enable.settings', // deprecated
51         'eg.hatch.enable.offline', // deprecated
52         'eg.cache',
53         'current_tag_table_marc21_biblio',
54         'FFPos',
55         'FFValue'
56     ];
57
58     service.keyStoredInBrowser = function(key) {
59
60         if (service.disableServerSettings) {
61             // When server-side storage is disabled, treat every
62             // setting like it's stored locally.
63             return true;
64         }
65
66         var browserOnly = false;
67         service.browserOnlyPrefixes.forEach(function(pfx) {
68             if (key.match(new RegExp('^' + pfx))) 
69                 browserOnly = true;
70         });
71
72         return browserOnly;
73     }
74
75     // write a message to the Hatch port
76     service.sendToHatch = function(msg) {
77         var msg2 = {};
78
79         // shallow copy and scrub msg before sending
80         angular.forEach(msg, function(val, key) {
81             if (key.match(/deferred/)) return;
82             msg2[key] = val;
83         });
84
85         console.debug("sending to Hatch: " + JSON.stringify(msg2));
86
87         msg2.from = 'page';
88         $window.postMessage(msg2, $window.location.origin);
89     }
90
91     // Send request to Hatch or reject if Hatch is unavailable
92     service.attemptHatchDelivery = function(msg) {
93         msg.msgid = service.msgId++;
94         msg.deferred = $q.defer();
95
96         if (service.hatchAvailable) {
97             service.messages[msg.msgid] = msg;
98             service.sendToHatch(msg);
99
100         } else {
101             console.error(
102                 'Hatch request attempted but Hatch is not available');
103             msg.deferred.reject(msg);
104         }
105
106         return msg.deferred.promise;
107     }
108
109
110     // resolve the promise on the given request and remove
111     // it from our tracked requests.
112     service.resolveRequest = function(msg) {
113
114         if (!service.messages[msg.msgid]) {
115             console.error('no cached message for id = ' + msg.msgid);
116             return;
117         }
118
119         // for requests sent through Hatch, only the cached 
120         // request will have the original promise attached
121         msg.deferred = service.messages[msg.msgid].deferred;
122         delete service.messages[msg.msgid]; // un-cache
123
124         if (msg.status == 200) {
125             msg.deferred.resolve(msg.content);
126         } else {
127             console.warn("Hatch command failed with status=" 
128                 + msg.status + " and message=" + msg.message);
129             msg.deferred.reject();
130         }
131     }
132
133     service.openHatch = function() {
134
135         // When the Hatch extension loads, it tacks an attribute onto
136         // the top-level documentElement to indicate it's available.
137         if (!$window.document.documentElement.getAttribute('hatch-is-open')) {
138             //console.debug("Hatch is not available");
139             return;
140         }
141
142         $window.addEventListener("message", function(event) {
143             // We only accept messages from our own content script.
144             if (event.source != window) return;
145
146             // We only care about messages from the Hatch extension.
147             if (event.data && event.data.from == 'extension') {
148
149                 // Avoid logging full Hatch responses. they can get large.
150                 console.debug(
151                     'Hatch responded to message ID ' + event.data.msgid);
152
153                 service.resolveRequest(event.data);
154             }
155         }); 
156
157         service.hatchAvailable = true; // public flag
158     }
159
160     service.remotePrint = function(
161         context, contentType, content, withDialog) {
162
163         return service.getPrintConfig(context).then(
164             function(config) {
165                 if (config.printer == 'hatch_file_writer') {
166                     if (contentType == 'text/html') {
167                         content = service.html2txt(content);
168                     }
169                     return service.setRemoteItem(
170                         'receipt.' + context + '.txt', content, true);
171                 } 
172                 // print configuration retrieved; print
173                 return service.attemptHatchDelivery({
174                     action : 'print',
175                     settings : config,
176                     content : content, 
177                     contentType : contentType,
178                     showDialog : withDialog,
179                 });
180             }
181         );
182     }
183
184     service.getPrintConfig = function(context) {
185         return service.getItem('eg.print.config.' + context);
186     }
187
188     service.setPrintConfig = function(context, config) {
189         return service.setItem('eg.print.config.' + context, config);
190     }
191
192     service.getPrinterOptions = function(name) {
193         return service.attemptHatchDelivery({
194             action : 'printer-options',
195             printer : name
196         });
197     }
198
199     service.getPrinters = function() {
200         if (service.printers) // cached printers
201             return $q.when(service.printers);
202
203         return service.attemptHatchDelivery({action : 'printers'}).then(
204
205             // we have remote printers; sort by name and return
206             function(printers) {
207                 service.printers = printers.sort(
208                     function(a,b) {return a.name < b.name ? -1 : 1});
209                 return service.printers;
210             },
211
212             // remote call failed and there is no such thing as local
213             // printers; return empty set.
214             function() { return [] } 
215         );
216     }
217
218     service.usePrinting = function() {
219         if (!service.hatchAvailable) {
220             return Promise.resolve(false);
221         }
222         return service.getItem('eg.hatch.enable.printing');
223     }
224
225     // DEPRECATED
226     service.useSettings = function() {
227         return service.getLocalItem('eg.hatch.enable.settings');
228     }
229
230     // DEPRECATED
231     service.useOffline = function() {
232         return service.getLocalItem('eg.hatch.enable.offline');
233     }
234
235     service.getWorkstations = function() {
236         if (service.hatchAvailable) {
237             return service.mergeWorkstations().then(
238                 function() {
239                     service.removeLocalItem('eg.workstation.all');
240                     return service.getRemoteItem('eg.workstation.all');
241                 }
242             );
243         } else {
244             return $q.when(service.getLocalItem('eg.workstation.all'));
245         }
246     }
247
248     // See if any workstations are stored in local storage.  If so, also
249     // see if we have any stored in Hatch.  If both, merged workstations
250     // from localStorage in Hatch storage, skipping any whose name
251     // collide with a workstation in Hatch.  If none exist in Hatch,
252     // copy the localStorage workstations over wholesale.
253     service.mergeWorkstations = function() {
254         var existing = service.getLocalItem('eg.workstation.all');
255
256         if (!existing || existing.length === 0) {
257             return $q.when();
258         }
259
260         return service.getRemoteItem('eg.workstation.all')
261         .then(function(inHatch) {
262
263             if (!inHatch || inHatch.length === 0) {
264                 // Nothing to merge, copy the data over directly
265                 console.debug('No workstations in hatch to merge');
266                 return service.setRemoteItem('eg.workstation.all', existing);
267             }
268
269             var addMe = [];
270             existing.forEach(function(ws) {
271                 var match = inHatch.filter(
272                     function(w) {return w.name === ws.name})[0];
273                 if (!match) {
274                     console.log(
275                         'Migrating workstation from local storage to hatch: ' 
276                         + ws.name
277                     );
278                     addMe.push(ws);
279                 }
280             });
281             inHatch = inHatch.concat(addMe);
282             return service.setRemoteItem('eg.workstation.all', inHatch);
283         });
284     }
285
286     service.getDefaultWorkstation = function() {
287
288         if (service.hatchAvailable) {
289             return service.getRemoteItem('eg.workstation.default')
290             .then(function(name) {
291                 if (name) {
292                     // We have a default in Hatch, remove any lingering
293                     // value from localStorage.
294                     service.removeLocalItem('eg.workstation.default');
295                     return name;
296                 }
297
298                 name = service.getLocalItem('eg.workstation.default');
299                 if (name) {
300                     console.log('Migrating default workstation to Hatch ' + name);
301                     return service.setRemoteItem('eg.workstation.default', name)
302                     .then(function() {return name;});
303                 }
304
305                 return null;
306             });
307         } else {
308             return $q.when(service.getLocalItem('eg.workstation.default'));
309         }
310     }
311
312     service.setWorkstations = function(workstations, isJson) {
313         if (service.hatchAvailable) {
314             return service.setRemoteItem('eg.workstation.all', workstations);
315         } else {
316             return $q.when(
317                 service.setLocalItem('eg.workstation.all', workstations, isJson));
318         }
319     }
320
321     service.setDefaultWorkstation = function(name, isJson) {
322         if (service.hatchAvailable) {
323             return service.setRemoteItem('eg.workstation.default', name);
324         } else {
325             return $q.when(
326                 service.setLocalItem('eg.workstation.default', name, isJson));
327         }
328     }
329
330     service.removeWorkstations = function() {
331         if (service.hatchAvailable) {
332             return service.removeRemoteItem('eg.workstation.all');
333         } else {
334             return $q.when(
335                 service.removeLocalItem('eg.workstation.all'));
336         }
337     }
338
339     service.removeDefaultWorkstation = function() {
340         if (service.hatchAvailable) {
341             return service.removeRemoteItem('eg.workstation.default');
342         } else {
343             return $q.when(
344                 service.removeLocalItem('eg.workstation.default'));
345         }
346     }
347
348
349     // Workstation actions always use Hatch when it's available
350     service.getWorkstationItem = function(key) {
351         if (service.hatchAvailable) {
352             return service.getRemoteItem(key);
353         } else {
354             return $q.when(service.getLocalItem(key));
355         }
356     }
357
358     service.setWorkstationItem = function(key, value) {
359         if (service.hatchAvailable) {
360             return service.setRemoteItem(key, value);
361         } else {
362             return $q.when(service.setLocalItem(key, value));
363         }
364     }
365
366     service.removeWorkstationItem = function(key) {
367         if (service.hatchAvailable) {
368             return service.removeRemoteItem(key);
369         } else {
370             return $q.when(service.removeLocalItem(key));
371         }
372     }
373
374     service.keyIsWorkstation = function(key) {
375         return Boolean(key.match(/eg.workstation/));
376     }
377
378     // get the value for a stored item
379     service.getItem = function(key) {
380
381         if (service.keyIsWorkstation(key)) {
382             return service.getWorkstationItem(key);
383         }
384
385         if (!service.keyStoredInBrowser(key)) {
386             return service.getServerItem(key);
387         }
388
389         var deferred = $q.defer();
390
391         service.getBrowserItem(key).then(
392             function(val) { deferred.resolve(val); },
393             function() { // Hatch error
394                 deferred.reject("Unable to getItem from Hatch: " + key);
395             }
396         );
397
398         return deferred.promise;
399     }
400
401     // Collect values in batch.
402     // For server-stored values espeically, this is more efficient 
403     // than a series of one-off calls.
404     service.getItemBatch = function(keys) {
405         var browserKeys = [];
406         var serverKeys = [];
407
408         // To take full advantage of the getServerItemBatch call,
409         // we have to know in advance which keys to send to the server
410         // vs those to handle in the browser.
411         keys.forEach(function(key) {
412             if (service.keyStoredInBrowser(key)) {
413                 browserKeys.push(key);
414             } else {
415                 serverKeys.push(key);
416             }
417         });
418
419         var settings = {};
420
421         var serverPromise = serverKeys.length === 0 ? $q.when() : 
422             service.getServerItemBatch(serverKeys).then(function(values) {
423                 angular.forEach(values, function(val, key) {
424                     settings[key] = val;
425                 });
426             });
427
428         var browserPromises = [];
429         browserKeys.forEach(function(key) {
430             browserPromises.push(
431                 service.getBrowserItem(key).then(function(val) {
432                     settings[key] = val;
433                 })
434             );
435         });
436
437         return $q.all(browserPromises.concat(serverPromise))
438             .then(function() {return settings});
439     }
440
441     service.getBrowserItem = function(key) {
442         if (service.useSettings()) {
443             if (service.hatchAvailable) {
444                 return service.getRemoteItem(key);
445             }
446         } else {
447             return $q.when(service.getLocalItem(key));
448         }
449         return $q.reject();
450     }
451
452     service.getRemoteItem = function(key) {
453         
454         if (service.keyCache[key] != undefined)
455             return $q.when(service.keyCache[key])
456
457         return service.attemptHatchDelivery({
458             key : key,
459             action : 'get'
460         }).then(function(content) {
461             return service.keyCache[key] = content;
462         });
463     }
464
465     service.getLocalItem = function(key) {
466         var val = $window.localStorage.getItem(key);
467         if (val === null || val === undefined) return;
468         try {
469             return JSON.parse(val);
470         } catch(E) {
471             console.error(
472                 "Deleting invalid JSON for localItem: " + key + " => " + val);
473             service.removeLocalItem(key);
474             return null;
475         }
476     }
477
478     // Force auth cookies to live under path "/" instead of "/eg/staff"
479     // so they may be shared with the Angular app.
480     // There's no way to tell under what path a cookie is stored in
481     // the browser, all we can do is migrate it regardless.
482     service.migrateAuthCookies = function() {
483         [   'eg.auth.token', 
484             'eg.auth.time', 
485             'eg.auth.token.oc', 
486             'eg.auth.time.oc'
487         ].forEach(function(key) {
488             var val = service.getLoginSessionItem(key);
489             if (val) {
490                 $cookies.remove(key, {path: '/eg/staff/'});
491                 service.setLoginSessionItem(key, val);
492             }
493         });
494     }
495
496     service.getLoginSessionItem = function(key) {
497         var val = $cookies.get(key);
498         if (val == null) return;
499         return JSON.parse(val);
500     }
501
502     service.getSessionItem = function(key) {
503         var val = $window.sessionStorage.getItem(key);
504         if (val == null) return;
505         return JSON.parse(val);
506     }
507
508     /**
509      * @param tmp bool Store the value as a session cookie only.  
510      * tmp values are removed during logout or browser close.
511      */
512     service.setItem = function(key, value) {
513
514         if (service.keyIsWorkstation(key)) {
515             return service.setWorkstationItem(key, value);
516         }
517
518         if (!service.keyStoredInBrowser(key)) {
519             return service.setServerItem(key, value);
520         }
521
522         var deferred = $q.defer();
523         return service.setBrowserItem(key, value).then(
524             function(val) {deferred.resolve(val);},
525
526             function() { // Hatch error
527                 deferred.reject("Unable to setItem in Hatch: " + key);
528             }
529         );
530     }
531
532     service.setBrowserItem = function(key, value) {
533         if (service.useSettings()) {
534             if (service.hatchAvailable) {
535                 return service.setRemoteItem(key, value);
536             } else {
537                 return $q.reject('Unable to get item from hatch');
538             }
539         } else {
540             return $q.when(service.setLocalItem(key, value));
541         }
542     }
543
544     service.setServerItem = function(key, value) {
545         if (!service.auth) service.auth = $injector.get('egAuth');
546         if (!service.auth.token()) return $q.when();
547
548         // If we have already attempted to retrieve a value for this
549         // setting, then we can tell up front whether applying a value
550         // at the server will be an option.  If not, store locally.
551         var summary = service.serverSettingSummaries[key];
552         if (summary && !summary.has_staff_setting) {
553
554             if (summary.has_org_setting === 't') {
555                 // When no user/ws setting types exist but an org unit
556                 // setting type does, it means the value cannot be
557                 // applied by an individual user.  Nothing left to do.
558                 return $q.when();
559             }
560
561             // No setting types of any flavor exist.
562             // Fall back to local storage.
563
564             if (value === null) {
565                 // a null value means clear the server setting.
566                 return service.removeBrowserItem(key);
567             } else {
568                 console.warn('No server setting type exists for ' + key);
569                 return service.setBrowserItem(key, value); 
570             }
571         }
572
573         var settings = {};
574         settings[key] = value;
575
576         return egNet.request(
577             'open-ils.actor',
578             'open-ils.actor.settings.apply.user_or_ws',
579             service.auth.token(), settings
580         ).then(function(appliedCount) {
581
582             if (appliedCount == 0) {
583                 console.warn('No server setting type exists for ' + key);
584                 // We were unable to store the setting on the server,
585                 // presumably becuase no server-side setting type exists.
586                 // Add to local storage instead.
587                 service.setLocalItem(key, value);
588             }
589
590             service.keyCache[key] = value;
591             return appliedCount;
592         });
593     }
594
595     service.getServerItem = function(key) {
596         if (key in service.keyCache) {
597             return $q.when(service.keyCache[key])
598         }
599
600         if (!service.auth) service.auth = $injector.get('egAuth');
601         if (!service.auth.token()) return $q.when(null);
602
603         return egNet.request(
604             'open-ils.actor',
605             'open-ils.actor.settings.retrieve.atomic',
606             [key], service.auth.token()
607         ).then(function(settings) {
608             return service.handleServerItemResponse(settings[0]);
609         });
610     }
611
612     service.handleServerItemResponse = function(summary) {
613         var key = summary.name;
614         var val = summary.value;
615
616         // For our purposes, we only care if a setting can be stored
617         // as an org setting or a user-or-workstation setting.
618         summary.has_staff_setting = (
619             summary.has_user_setting === 't' || 
620             summary.has_workstation_setting === 't'
621         );
622
623         summary.value = null; // avoid duplicate value caches
624         service.serverSettingSummaries[key] = summary;
625
626         if (val !== null) {
627             // We have a server setting.  Nothing left to do.
628             return $q.when(service.keyCache[key] = val);
629         }
630
631         if (!summary.has_staff_setting) {
632
633             if (summary.has_org_setting === 't') {
634                 // An org unit setting type exists but no value is applied
635                 // that this workstation has access to.  The existence of 
636                 // an org unit setting type and no user/ws setting type 
637                 // means applying a value locally is not allowed.  
638                 return $q.when(service.keyCache[key] = undefined);
639             }
640
641             console.warn('No server setting type exists for ' 
642                 + key + ', using local value.');
643
644             return service.getBrowserItem(key);
645         }
646
647         // A user/ws setting type exists, but no server value exists.
648         // Migrate the local setting to the server.
649
650         var deferred = $q.defer();
651         service.getBrowserItem(key).then(function(browserVal) {
652
653             if (browserVal === null || browserVal === undefined) {
654                 // No local value to migrate.
655                 return deferred.resolve(service.keyCache[key] = undefined);
656             }
657
658             // Migrate the local value to the server.
659
660             service.setServerItem(key, browserVal).then(
661                 function(appliedCount) {
662                     if (appliedCount == 1) {
663                         console.info('setting ' + key + ' successfully ' +
664                             'migrated to a server setting');
665                         service.removeBrowserItem(key); // fire & forget
666                     } else {
667                         console.error('error migrating setting to server,' 
668                             + ' falling back to local value');
669                     }
670                     deferred.resolve(service.keyCache[key] = browserVal);
671                 }
672             );
673         });
674
675         return deferred.promise;
676     }
677
678     service.getServerItemBatch = function(keys) {
679         // no cache checking for now.  assumes batch mode is only
680         // called once on page load.  maybe add cache checking later.
681         if (!service.auth) service.auth = $injector.get('egAuth');
682         if (!service.auth.token()) return $q.when({});
683
684         var foundValues = {};
685         return egNet.request(
686             'open-ils.actor',
687             'open-ils.actor.settings.retrieve.atomic',
688             keys, service.auth.token()
689         ).then(
690             function(settings) { 
691                 //return foundValues; 
692
693                 var deferred = $q.defer();
694                 function checkOne(setting) {
695                     if (!setting) {
696                         deferred.resolve(foundValues);
697                         return;
698                     }
699                     service.handleServerItemResponse(setting)
700                     .then(function(resp) {
701                         if (resp !== undefined) {
702                             foundValues[setting.name] = resp;
703                         }
704                         settings.shift();
705                         checkOne(settings[0]);
706                     });
707                 }
708
709                 checkOne(settings[0]);
710                 return deferred.promise;
711             }
712         );
713     }
714
715
716     // set the value for a stored or new item
717     // When "bare" is true, the value will not be JSON-encoded
718     // on the file system.
719     service.setRemoteItem = function(key, value, bare) {
720         service.keyCache[key] = value;
721         return service.attemptHatchDelivery({
722             key : key, 
723             content : value, 
724             action : 'set',
725             bare: bare
726         });
727     }
728
729     // Set the value for the given key.
730     // "Local" items persist indefinitely.
731     // If the value is raw, pass it as 'value'.  If it was
732     // externally JSONified, pass it via jsonified.
733     service.setLocalItem = function(key, value, jsonified) {
734         if (jsonified === undefined ) {
735             jsonified = JSON.stringify(value);
736         } else if (value === undefined) {
737             return;
738         }
739         try {
740             $window.localStorage.setItem(key, jsonified);
741         } catch (e) {
742             console.log('localStorage.setItem (overwrite) failed for '+key+': ', e);
743         }
744     }
745
746     service.appendItem = function(key, value) {
747         if (!service.useSettings())
748             return $q.when(service.appendLocalItem(key, value));
749
750         if (service.hatchAvailable)
751             return service.appendRemoteItem(key, value);
752
753         console.error("Unable to appendItem in Hatch: " + key);
754         return $q.reject();
755     }
756
757     // append the value to a stored or new item
758     service.appendRemoteItem = function(key, value) {
759         service.keyCache[key] = value;
760         return service.attemptHatchDelivery({
761             key : key, 
762             content : value, 
763             action : 'append',
764         });
765     }
766
767     service.appendLocalItem = function(key, value, jsonified) {
768         if (jsonified === undefined ) 
769             jsonified = JSON.stringify(value);
770
771         var old_value = $window.localStorage.getItem(key) || '';
772         try {
773             $window.localStorage.setItem( key, old_value + jsonified );
774         } catch (e) {
775             console.log('localStorage.setItem (append) failed for '+key+': ', e);
776         }
777     }
778
779     // Set the value for the given key.  
780     // "LoginSession" items are removed when the user logs out or the 
781     // browser is closed.
782     // If the value is raw, pass it as 'value'.  If it was
783     // externally JSONified, pass it via jsonified.
784     service.setLoginSessionItem = function(key, value, jsonified) {
785         service.addLoginSessionKey(key);
786         if (jsonified === undefined ) 
787             jsonified = JSON.stringify(value);
788         $cookies.put(key, jsonified, {path: '/'});
789     }
790
791     // Set the value for the given key.  
792     // "Session" items are browser tab-specific and are removed when the
793     // tab is closed.
794     // If the value is raw, pass it as 'value'.  If it was
795     // externally JSONified, pass it via jsonified.
796     service.setSessionItem = function(key, value, jsonified) {
797         if (jsonified === undefined ) 
798             jsonified = JSON.stringify(value);
799         $window.sessionStorage.setItem(key, jsonified);
800     }
801
802     // remove a stored item
803     service.removeItem = function(key) {
804
805         if (service.keyIsWorkstation(key)) {
806             return service.removeWorkstationItem(key);
807         }
808
809         if (!service.keyStoredInBrowser(key)) {
810             return service.removeServerItem(key);
811         }
812
813         var deferred = $q.defer();
814         service.removeBrowserItem(key).then(
815             function(response) {deferred.resolve(response);},
816             function() { // Hatch error
817                 deferred.reject("Unable to removeItem from Hatch: " + key);
818             }
819         );
820
821         return deferred.promise;
822     }
823
824     service.removeBrowserItem = function(key) {
825         if (service.useSettings()) {
826             if (service.hatchAvailable) {
827                 return service.removeRemoteItem(key);
828             } else {
829                 return $q.reject('error talking to Hatch');
830             }
831         } else {
832             return $q.when(service.removeLocalItem(key));
833         }
834     }
835
836     service.removeServerItem = function(key) {
837         return service.setServerItem(key, null);
838     }
839
840     service.removeRemoteItem = function(key) {
841         delete service.keyCache[key];
842         return service.attemptHatchDelivery({
843             key : key,
844             action : 'remove'
845         });
846     }
847
848     service.removeLocalItem = function(key) {
849         $window.localStorage.removeItem(key);
850     }
851
852     service.removeLoginSessionItem = function(key) {
853         service.removeLoginSessionKey(key);
854         $cookies.remove(key, {path: '/'});
855     }
856
857     service.removeSessionItem = function(key) {
858         $window.sessionStorage.removeItem(key);
859     }
860
861     /**
862      * Remove all "LoginSession" items.
863      */
864     service.clearLoginSessionItems = function() {
865         angular.forEach(service.getLoginSessionKeys(), function(key) {
866             service.removeLoginSessionItem(key);
867         });
868
869         // remove the keys cache.
870         service.removeLocalItem('eg.hatch.login_keys');
871     }
872
873     // if set, prefix limits the return set to keys starting with 'prefix'
874     service.getKeys = function(prefix) {
875         var promise = service.getServerKeys(prefix);
876         return service.getBrowserKeys(prefix).then(function(browserKeys) {
877             return promise.then(function(serverKeys) {
878                 return serverKeys.concat(browserKeys);
879             });
880         });
881     }
882
883     service.getRemoteKeys = function(prefix) {
884         return service.attemptHatchDelivery({
885             key : prefix,
886             action : 'keys'
887         });
888     }
889
890     service.getBrowserKeys = function(prefix) {
891         if (service.useSettings()) 
892             return service.getRemoteKeys(prefix);
893         return $q.when(service.getLocalKeys(prefix));
894     }
895
896     service.getServerKeys = function(prefix, options) {
897         if (!service.auth) service.auth = $injector.get('egAuth');
898         if (!service.auth.token()) return $q.when({});
899         return egNet.request(
900             'open-ils.actor',
901             'open-ils.actor.settings.staff.applied.names.authoritative.atomic',
902             service.auth.token(), prefix, options
903         );
904     }
905
906     service.getLocalKeys = function(prefix) {
907         var keys = [];
908         var idx = 0;
909         while ( (k = $window.localStorage.key(idx++)) !== null) {
910             // key prefix match test
911             if (prefix && k.substr(0, prefix.length) != prefix) continue; 
912             keys.push(k);
913         }
914         return keys;
915     }
916
917
918     /**
919      * Array of "LoginSession" keys.
920      * Note we have to store these as "Local" items so browser tabs can
921      * share them.  We could store them as cookies, but it's more data
922      * that has to go back/forth to the server.  A "LoginSession" key name is
923      * not private, though, so it's OK if they are left in localStorage
924      * until the next login.
925      */
926     service.getLoginSessionKeys = function(prefix) {
927         var keys = [];
928         var idx = 0;
929         var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
930         angular.forEach(login_keys, function(k) {
931             // key prefix match test
932             if (prefix && k.substr(0, prefix.length) != prefix) return;
933             keys.push(k);
934         });
935         return keys;
936     }
937
938     service.addLoginSessionKey = function(key) {
939         var keys = service.getLoginSessionKeys();
940         if (keys.indexOf(key) < 0) {
941             keys.push(key);
942             service.setLocalItem('eg.hatch.login_keys', keys);
943         }
944     }
945
946     service.removeLoginSessionKey = function(key) {
947         var keys = service.getLoginSessionKeys().filter(function(k) {
948             return k != key;
949         });
950         service.setLocalItem('eg.hatch.login_keys', keys);
951     }
952
953     // Copy all stored settings from localStorage to Hatch.
954     // If 'move' is true, delete the local settings once cloned.
955     service.copySettingsToHatch = function(move) {
956         var deferred = $q.defer();
957         var keys = service.getLocalKeys();
958
959         angular.forEach(keys, function(key) {
960
961             // Hatch keys are local-only
962             if (key.match(/^eg.hatch/)) return;
963
964             console.debug("Copying to Hatch Storage: " + key);
965             service.setRemoteItem(key, service.getLocalItem(key))
966             .then(function() { // key successfully cloned.
967
968                 // delete the local copy if requested.
969                 if (move) service.removeLocalItem(key);
970
971                 // resolve the promise after processing the last key.
972                 if (key == keys[keys.length-1]) 
973                     deferred.resolve();
974             });
975         });
976
977         return deferred.promise;
978     }
979
980     // Copy all stored settings from Hatch to localStorage.
981     // If 'move' is true, delete the Hatch settings once cloned.
982     service.copySettingsToLocal = function(move) {
983         var deferred = $q.defer();
984
985         service.getRemoteKeys().then(function(keys) {
986             angular.forEach(keys, function(key) {
987                 service.getRemoteItem(key).then(function(val) {
988
989                     console.debug("Copying to Local Storage: " + key);
990                     service.setLocalItem(key, val);
991
992                     // delete the remote copy if requested.
993                     if (move) service.removeRemoteItem(key);
994
995                     // resolve the promise after processing the last key.
996                     if (key == keys[keys.length-1]) 
997                         deferred.resolve();
998                 });
999             });
1000         });
1001
1002         return deferred.promise;
1003     }
1004
1005     service.hostname = function() {
1006         if (service.hatchAvailable) {
1007             return service.attemptHatchDelivery({action : 'hostname'})
1008             .then(
1009                 function(name) { return name; },
1010                 // Gracefully handle case where Hatch has not yet been 
1011                 // updated to include the hostname command.
1012                 function() {return null}
1013             );
1014         } 
1015         return $q.when(null);
1016     }
1017
1018     // COPIED FROM XUL util/text.js
1019     service.reverse_preserve_string_in_html = function( text ) {
1020         text = text.replace(/&amp;/g, '&');
1021         text = text.replace(/&quot;/g, '"');
1022         text = text.replace(/&#39;/g, "'");
1023         text = text.replace(/&nbsp;/g, ' ');
1024         text = text.replace(/&lt;/g, '<');
1025         text = text.replace(/&gt;/g, '>');
1026         return text;
1027     }
1028
1029     // COPIED FROM XUL util/print.js
1030     service.html2txt = function(html) {
1031         var lines = html.split(/\n/);
1032         var new_lines = [];
1033         for (var i = 0; i < lines.length; i++) {
1034             var line = lines[i];
1035             if (!line) {
1036                 new_lines.push(line);
1037                 continue;
1038             }
1039
1040             // This undoes the util.text.preserve_string_in_html 
1041             // call that spine_label.js does
1042             line = service.reverse_preserve_string_in_html(line);
1043
1044             // This looks for @hex attributes containing 2-digit hex 
1045             // codes, and converts them into real characters
1046             line = line.replace(/(<.+?)hex=['"](.+?)['"](.*?>)/gi, 
1047                 function(str,p1,p2,p3,offset,s) {
1048
1049                 var raw_chars = '';
1050                 var hex_chars = p2.match(/[0-9,a-f,A-F][0-9,a-f,A-F]/g);
1051                 for (var j = 0; j < hex_chars.length; j++) {
1052                     raw_chars += String.fromCharCode( parseInt(hex_chars[j],16) );
1053                 }
1054                 return p1 + p3 + raw_chars;
1055             });
1056
1057             line = line.replace(/<head.*?>.*?<\/head>/gi, '');
1058             line = line.replace(/<br.*?>/gi,'\r\n');
1059             line = line.replace(/<table.*?>/gi,'');
1060             line = line.replace(/<tr.*?>/gi,'');
1061             line = line.replace(/<hr.*?>/gi,'\r\n');
1062             line = line.replace(/<p.*?>/gi,'');
1063             line = line.replace(/<block.*?>/gi,'');
1064             line = line.replace(/<li.*?>/gi,' * ');
1065             line = line.replace(/<.+?>/gi,'');
1066             if (line) { new_lines.push(line); }
1067         }
1068
1069         return new_lines.join('\n');
1070     }
1071
1072     // The only requirement for opening Hatch is that the DOM be loaded.
1073     // Open the connection now so its state will be immediately available.
1074     service.openHatch();
1075
1076     return service;
1077 }])
1078