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.state = 'IDLE'; // IDLE, INIT, CONNECTED, NO_CONNECTION
38 // write a message to the Hatch port
39 service.sendToHatch = function(msg) {
42 // shallow copy and scrub msg before sending
43 angular.forEach(msg, function(val, key) {
44 if (key.match(/deferred/)) return;
48 console.debug("sending to Hatch: " + JSON.stringify(msg2,null,2));
51 $window.postMessage(msg2, $window.location.origin);
54 // Send the request to Hatch if it's available.
55 // Otherwise handle the request locally.
56 service.attemptHatchDelivery = function(msg) {
58 msg.msgid = service.msgId++;
59 msg.deferred = $q.defer();
61 if (service.state == 'NO_CONNECTION') {
62 msg.deferred.reject(msg);
64 } else if (service.state.match(/CONNECTED|INIT/)) {
65 // Hatch is known to be open
66 service.messages[msg.msgid] = msg;
67 service.sendToHatch(msg);
69 } else if (service.state == 'IDLE') {
70 service.messages[msg.msgid] = msg;
71 service.pending.push(msg);
72 $timeout(service.openHatch);
75 return msg.deferred.promise;
79 // resolve the promise on the given request and remove
80 // it from our tracked requests.
81 service.resolveRequest = function(msg) {
83 if (!service.messages[msg.msgid]) {
84 console.warn('no cached message for '
85 + msg.msgid + ' : ' + JSON.stringify(msg, null, 2));
89 // for requests sent through Hatch, only the cached
90 // request will have the original promise attached
91 msg.deferred = service.messages[msg.msgid].deferred;
92 delete service.messages[msg.msgid]; // un-cache
94 switch (service.state) {
96 case 'CONNECTED': // received a standard Hatch response
97 if (msg.status == 200) {
98 msg.deferred.resolve(msg.content);
100 msg.deferred.reject();
101 console.warn("Hatch command failed with status="
102 + msg.status + " and message=" + msg.message);
107 if (msg.status == 200) {
108 service.hatchAvailable = true; // public flag
109 service.state = 'CONNECTED';
110 service.hatchOpened();
112 msg.deferred.reject();
113 service.hatchWontOpen(msg.message);
119 "Received message in unexpected state: " + service.state);
123 service.openHatch = function() {
125 // When the Hatch extension loads, it tacks an attribute onto
126 // the page body to indicate it's available.
128 if (!$window.document.body.getAttribute('hatch-is-open')) {
129 service.hatchWontOpen('Hatch is not available');
133 $window.addEventListener("message", function(event) {
134 // We only accept messages from our own content script.
135 if (event.source != window) return;
137 // We only care about messages from the Hatch extension.
138 if (event.data && event.data.from == 'extension') {
140 console.debug('Hatch says: '
141 + JSON.stringify(event.data, null, 2));
143 service.resolveRequest(event.data);
147 service.state = 'INIT';
148 service.attemptHatchDelivery({action : 'init'});
151 service.hatchWontOpen = function(err) {
152 console.debug("Hatch connection failed: " + err);
153 service.state = 'NO_CONNECTION';
154 service.hatchAvailable = false;
155 service.hatchClosed();
158 service.hatchClosed = function() {
159 service.printers = [];
160 service.printConfig = {};
161 while ( (msg = service.pending.shift()) ) {
162 msg.deferred.reject(msg);
163 delete service.messages[msg.msgid];
165 if (service.onHatchClose)
166 service.onHatchClose();
169 // Returns true if Hatch is required or if we are currently
170 // communicating with the Hatch service.
171 service.usingHatch = function() {
172 return service.state == 'CONNECTED' || service.hatchRequired();
175 // Returns true if this browser (via localStorage) is
176 // configured to require Hatch.
177 service.hatchRequired = function() {
178 return service.getLocalItem('eg.hatch.required');
181 service.hatchOpened = function() {
182 // let others know we're connected
183 if (service.onHatchOpen) service.onHatchOpen();
185 // Deliver any previously queued requests
186 while ( (msg = service.pending.shift()) ) {
187 service.sendToHatch(msg);
191 service.getPrintConfig = function() {
192 if (service.printConfig)
193 return $q.when(service.printConfig);
195 return service.getRemoteItem('eg.print.config')
196 .then(function(conf) {
197 return (service.printConfig = conf || {})
201 service.setPrintConfig = function(conf) {
202 service.printConfig = conf;
203 return service.setRemoteItem('eg.print.config', conf);
207 service.remotePrint = function(
208 context, contentType, content, withDialog) {
210 return service.getPrintConfig().then(
212 // print configuration retrieved; print
213 return service.attemptHatchDelivery({
215 config : conf[context],
217 contentType : contentType,
218 showDialog : withDialog,
224 // launch the print dialog then attach the resulting configuration
225 // to the requested context, then store the final values.
226 service.configurePrinter = function(context, printer) {
228 // load current settings
229 return service.getPrintConfig()
231 // dispatch the print configuration request
232 .then(function(config) {
234 // loaded remote config
235 if (!config[context]) config[context] = {};
236 config[context].printer = printer;
237 return service.attemptHatchDelivery({
239 action : 'print-config',
240 config : config[context]
244 // set the returned settings to the requested context
245 .then(function(newconf) {
246 if (angular.isObject(newconf)) {
247 newconf.printer = printer;
248 return service.printConfig[context] = newconf;
250 console.warn("configurePrinter() returned " + newconf);
254 // store the newly linked settings
256 service.setItem('eg.print.config', service.printConfig);
259 // return the final settings to the caller
260 .then(function() {return service.printConfig});
263 service.getPrinters = function() {
264 if (service.printers) // cached printers
265 return $q.when(service.printers);
267 return service.attemptHatchDelivery({action : 'printers'}).then(
269 // we have remote printers; sort by name and return
271 service.printers = printers.sort(
272 function(a,b) {return a.name < b.name ? -1 : 1});
273 return service.printers;
276 // remote call failed and there is no such thing as local
277 // printers; return empty set.
278 function() { return [] }
282 // get the value for a stored item
283 service.getItem = function(key) {
284 return service.getRemoteItem(key)['catch'](
286 if (service.hatchRequired()) {
287 console.error("Unable to getItem: " + key
288 + "; hatchRequired=true, but hatch is not connected");
291 return service.getLocalItem(msg.key);
296 service.getRemoteItem = function(key) {
297 return service.attemptHatchDelivery({
303 service.getLocalItem = function(key) {
304 var val = $window.localStorage.getItem(key);
305 if (val == null) return;
306 return JSON.parse(val);
309 service.getLoginSessionItem = function(key) {
310 var val = $cookies.get(key);
311 if (val == null) return;
312 return JSON.parse(val);
315 service.getSessionItem = function(key) {
316 var val = $window.sessionStorage.getItem(key);
317 if (val == null) return;
318 return JSON.parse(val);
322 * @param tmp bool Store the value as a session cookie only.
323 * tmp values are removed during logout or browser close.
325 service.setItem = function(key, value) {
326 return service.setRemoteItem(key, value)['catch'](
328 if (service.hatchRequired()) {
329 console.error("Unable to setItem: " + key
330 + "; hatchRequired=true, but hatch is not connected");
333 return service.setLocalItem(msg.key, value);
338 // set the value for a stored or new item
339 service.setRemoteItem = function(key, value) {
340 return service.attemptHatchDelivery({
347 // Set the value for the given key.
348 // "Local" items persist indefinitely.
349 // If the value is raw, pass it as 'value'. If it was
350 // externally JSONified, pass it via jsonified.
351 service.setLocalItem = function(key, value, jsonified) {
352 if (jsonified === undefined )
353 jsonified = JSON.stringify(value);
354 $window.localStorage.setItem(key, jsonified);
357 // Set the value for the given key.
358 // "LoginSession" items are removed when the user logs out or the
359 // browser is closed.
360 // If the value is raw, pass it as 'value'. If it was
361 // externally JSONified, pass it via jsonified.
362 service.setLoginSessionItem = function(key, value, jsonified) {
363 service.addLoginSessionKey(key);
364 if (jsonified === undefined )
365 jsonified = JSON.stringify(value);
366 $cookies.put(key, jsonified);
369 // Set the value for the given key.
370 // "Session" items are browser tab-specific and are removed when the
372 // If the value is raw, pass it as 'value'. If it was
373 // externally JSONified, pass it via jsonified.
374 service.setSessionItem = function(key, value, jsonified) {
375 if (jsonified === undefined )
376 jsonified = JSON.stringify(value);
377 $window.sessionStorage.setItem(key, jsonified);
380 // assumes the appender and appendee are both strings
381 // TODO: support arrays as well
382 service.appendLocalItem = function(key, value) {
383 var item = service.getLocalItem(key);
385 if (typeof item != 'string') {
386 logger.warn("egHatch.appendLocalItem => "
387 + "cannot append to a non-string item: " + key);
390 value = item + value; // concatenate our value
392 service.setLocalitem(key, value);
395 // remove a stored item
396 service.removeItem = function(key) {
397 return service.removeRemoteItem(key)['catch'](
399 return service.removeLocalItem(msg.key)
404 service.removeRemoteItem = function(key) {
405 return service.attemptHatchDelivery({
411 service.removeLocalItem = function(key) {
412 $window.localStorage.removeItem(key);
415 service.removeLoginSessionItem = function(key) {
416 service.removeLoginSessionKey(key);
417 $cookies.remove(key);
420 service.removeSessionItem = function(key) {
421 $window.sessionStorage.removeItem(key);
425 * Remove all "LoginSession" items.
427 service.clearLoginSessionItems = function() {
428 angular.forEach(service.getLoginSessionKeys(), function(key) {
429 service.removeLoginSessionItem(key);
432 // remove the keys cache.
433 service.removeLocalItem('eg.hatch.login_keys');
436 // if set, prefix limits the return set to keys starting with 'prefix'
437 service.getKeys = function(prefix) {
438 return service.getRemoteKeys(prefix)['catch'](
440 if (service.hatchRequired()) {
441 console.error("Unable to get pref keys; "
442 + "hatchRequired=true, but hatch is not connected");
445 return service.getLocalKeys(prefix)
450 service.getRemoteKeys = function(prefix) {
451 return service.attemptHatchDelivery({
457 service.getLocalKeys = function(prefix) {
460 while ( (k = $window.localStorage.key(idx++)) !== null) {
461 // key prefix match test
462 if (prefix && k.substr(0, prefix.length) != prefix) continue;
470 * Array of "LoginSession" keys.
471 * Note we have to store these as "Local" items so browser tabs can
472 * share them. We could store them as cookies, but it's more data
473 * that has to go back/forth to the server. A "LoginSession" key name is
474 * not private, though, so it's OK if they are left in localStorage
475 * until the next login.
477 service.getLoginSessionKeys = function(prefix) {
480 var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
481 angular.forEach(login_keys, function(k) {
482 // key prefix match test
483 if (prefix && k.substr(0, prefix.length) != prefix) return;
489 service.addLoginSessionKey = function(key) {
490 var keys = service.getLoginSessionKeys();
491 if (keys.indexOf(key) < 0) {
493 service.setLocalItem('eg.hatch.login_keys', keys);
497 service.removeLoginSessionKey = function(key) {
498 var keys = service.getLoginSessionKeys().filter(function(k) {
501 service.setLocalItem('eg.hatch.login_keys', keys);