2 * Core Service - egHatch
4 * Dispatches print and data storage requests to the appropriate handler.
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.
11 * Most handlers also provide direct remote and local variants to the
12 * application can decide to which to use as needed.
14 * Local storage requests are handled by $window.localStorage.
16 * Note that all top-level and remote requests return promises. All
17 * local requests return immediate values, since local requests are
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()
25 angular.module('egCoreMod')
28 ['$q','$window','$timeout','$interpolate','$http','$cookies',
29 function($q , $window , $timeout , $interpolate , $http , $cookies) {
33 service.messages = {};
35 service.hatchAvailable = null;
36 service.cachedPrintConfig = {};
37 service.state = 'IDLE'; // IDLE, INIT, CONNECTED, NO_CONNECTION
39 // write a message to the Hatch port
40 service.sendToHatch = function(msg) {
43 // shallow copy and scrub msg before sending
44 angular.forEach(msg, function(val, key) {
45 if (key.match(/deferred/)) return;
49 console.debug("sending to Hatch: " + JSON.stringify(msg2,null,2));
52 $window.postMessage(msg2, $window.location.origin);
55 // Send the request to Hatch if it's available.
56 // Otherwise handle the request locally.
57 service.attemptHatchDelivery = function(msg) {
59 msg.msgid = service.msgId++;
60 msg.deferred = $q.defer();
62 if (service.state == 'NO_CONNECTION') {
63 msg.deferred.reject(msg);
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);
70 } else if (service.state == 'IDLE') {
71 service.messages[msg.msgid] = msg;
72 service.pending.push(msg);
73 $timeout(service.openHatch);
76 return msg.deferred.promise;
80 // resolve the promise on the given request and remove
81 // it from our tracked requests.
82 service.resolveRequest = function(msg) {
84 if (!service.messages[msg.msgid]) {
85 console.warn('no cached message for '
86 + msg.msgid + ' : ' + JSON.stringify(msg, null, 2));
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
95 switch (service.state) {
97 case 'CONNECTED': // received a standard Hatch response
98 if (msg.status == 200) {
99 msg.deferred.resolve(msg.content);
101 msg.deferred.reject();
102 console.warn("Hatch command failed with status="
103 + msg.status + " and message=" + msg.message);
108 if (msg.status == 200) {
109 service.hatchAvailable = true; // public flag
110 service.state = 'CONNECTED';
111 service.hatchOpened();
113 msg.deferred.reject();
114 service.hatchWontOpen(msg.message);
120 "Received message in unexpected state: " + service.state);
124 service.openHatch = function() {
126 // When the Hatch extension loads, it tacks an attribute onto
127 // the page body to indicate it's available.
129 if (!$window.document.body.getAttribute('hatch-is-open')) {
130 service.hatchWontOpen('Hatch is not available');
134 $window.addEventListener("message", function(event) {
135 // We only accept messages from our own content script.
136 if (event.source != window) return;
138 // We only care about messages from the Hatch extension.
139 if (event.data && event.data.from == 'extension') {
141 console.debug('Hatch says: '
142 + JSON.stringify(event.data, null, 2));
144 service.resolveRequest(event.data);
148 service.state = 'INIT';
149 service.attemptHatchDelivery({action : 'init'});
152 service.hatchWontOpen = function(err) {
153 console.debug("Hatch connection failed: " + err);
154 service.state = 'NO_CONNECTION';
155 service.hatchAvailable = false;
156 service.hatchClosed();
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];
166 if (service.onHatchClose)
167 service.onHatchClose();
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();
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');
182 service.hatchOpened = function() {
183 // let others know we're connected
184 if (service.onHatchOpen) service.onHatchOpen();
186 // Deliver any previously queued requests
187 while ( (msg = service.pending.shift()) ) {
188 service.sendToHatch(msg);
192 service.remotePrint = function(
193 context, contentType, content, withDialog) {
195 return service.getPrintConfig(context).then(
197 // print configuration retrieved; print
198 return service.attemptHatchDelivery({
202 contentType : contentType,
203 showDialog : withDialog,
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])
214 return service.getRemoteItem('eg.print.config.' + context)
215 .then(function(config) {
216 return service.cachedPrintConfig[context] = config;
220 service.setPrintConfig = function(context, config) {
221 service.cachedPrintConfig[context] = config;
222 return service.setRemoteItem('eg.print.config.' + context, config);
225 service.getPrinterOptions = function(name) {
226 return service.attemptHatchDelivery({
227 action : 'printer-options',
232 service.getPrinters = function() {
233 if (service.printers) // cached printers
234 return $q.when(service.printers);
236 return service.attemptHatchDelivery({action : 'printers'}).then(
238 // we have remote printers; sort by name and return
240 service.printers = printers.sort(
241 function(a,b) {return a.name < b.name ? -1 : 1});
242 return service.printers;
245 // remote call failed and there is no such thing as local
246 // printers; return empty set.
247 function() { return [] }
251 // get the value for a stored item
252 service.getItem = function(key) {
253 return service.getRemoteItem(key)['catch'](
255 if (service.hatchRequired()) {
256 console.error("Unable to getItem: " + key
257 + "; hatchRequired=true, but hatch is not connected");
260 return service.getLocalItem(msg.key);
265 service.getRemoteItem = function(key) {
266 return service.attemptHatchDelivery({
272 service.getLocalItem = function(key) {
273 var val = $window.localStorage.getItem(key);
274 if (val == null) return;
275 return JSON.parse(val);
278 service.getLoginSessionItem = function(key) {
279 var val = $cookies.get(key);
280 if (val == null) return;
281 return JSON.parse(val);
284 service.getSessionItem = function(key) {
285 var val = $window.sessionStorage.getItem(key);
286 if (val == null) return;
287 return JSON.parse(val);
291 * @param tmp bool Store the value as a session cookie only.
292 * tmp values are removed during logout or browser close.
294 service.setItem = function(key, value) {
295 return service.setRemoteItem(key, value)['catch'](
297 if (service.hatchRequired()) {
298 console.error("Unable to setItem: " + key
299 + "; hatchRequired=true, but hatch is not connected");
302 return service.setLocalItem(msg.key, value);
307 // set the value for a stored or new item
308 service.setRemoteItem = function(key, value) {
309 return service.attemptHatchDelivery({
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);
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);
338 // Set the value for the given key.
339 // "Session" items are browser tab-specific and are removed when the
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);
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);
354 if (typeof item != 'string') {
355 logger.warn("egHatch.appendLocalItem => "
356 + "cannot append to a non-string item: " + key);
359 value = item + value; // concatenate our value
361 service.setLocalitem(key, value);
364 // remove a stored item
365 service.removeItem = function(key) {
366 return service.removeRemoteItem(key)['catch'](
368 return service.removeLocalItem(msg.key)
373 service.removeRemoteItem = function(key) {
374 return service.attemptHatchDelivery({
380 service.removeLocalItem = function(key) {
381 $window.localStorage.removeItem(key);
384 service.removeLoginSessionItem = function(key) {
385 service.removeLoginSessionKey(key);
386 $cookies.remove(key);
389 service.removeSessionItem = function(key) {
390 $window.sessionStorage.removeItem(key);
394 * Remove all "LoginSession" items.
396 service.clearLoginSessionItems = function() {
397 angular.forEach(service.getLoginSessionKeys(), function(key) {
398 service.removeLoginSessionItem(key);
401 // remove the keys cache.
402 service.removeLocalItem('eg.hatch.login_keys');
405 // if set, prefix limits the return set to keys starting with 'prefix'
406 service.getKeys = function(prefix) {
407 return service.getRemoteKeys(prefix)['catch'](
409 if (service.hatchRequired()) {
410 console.error("Unable to get pref keys; "
411 + "hatchRequired=true, but hatch is not connected");
414 return service.getLocalKeys(prefix)
419 service.getRemoteKeys = function(prefix) {
420 return service.attemptHatchDelivery({
426 service.getLocalKeys = function(prefix) {
429 while ( (k = $window.localStorage.key(idx++)) !== null) {
430 // key prefix match test
431 if (prefix && k.substr(0, prefix.length) != prefix) continue;
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.
446 service.getLoginSessionKeys = function(prefix) {
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;
458 service.addLoginSessionKey = function(key) {
459 var keys = service.getLoginSessionKeys();
460 if (keys.indexOf(key) < 0) {
462 service.setLocalItem('eg.hatch.login_keys', keys);
466 service.removeLoginSessionKey = function(key) {
467 var keys = service.getLoginSessionKeys().filter(function(k) {
470 service.setLocalItem('eg.hatch.login_keys', keys);