GUI -- Added websock open listeners mechanism.
- made event handler structure parsing a little cleverer.

Change-Id: I15120dabd0aade28e6ee3680faf96f408aed1450
diff --git a/web/gui/src/main/webapp/app/fw/remote/websocket.js b/web/gui/src/main/webapp/app/fw/remote/websocket.js
index ca2840d..cb68098 100644
--- a/web/gui/src/main/webapp/app/fw/remote/websocket.js
+++ b/web/gui/src/main/webapp/app/fw/remote/websocket.js
@@ -30,10 +30,13 @@
         sid = 0,                // event sequence identifier
         handlers = {},          // event handler bindings
         pendingEvents = [],     // events TX'd while socket not up
+        host,                   // web socket host
         url,                    // web socket URL
         clusterNodes = [],      // ONOS instances data for failover
         clusterIndex = -1,      // the instance to which we are connected
-        connectRetries = 0;
+        connectRetries = 0,     // limit our attempts at reconnecting
+        openListeners = {},     // registered listeners for websocket open()
+        nextListenerId = 1;     // internal ID for open listeners
 
     // =======================
     // === Bootstrap Handler
@@ -55,7 +58,7 @@
     // === Web socket callbacks
 
     function handleOpen() {
-        $log.info('Web socket open');
+        $log.info('Web socket open - ', url);
         vs.hide();
 
         $log.debug('Sending ' + pendingEvents.length + ' pending event(s)...');
@@ -66,6 +69,7 @@
 
         connectRetries = 0;
         wsUp = true;
+        informListeners(host, url);
     }
 
     // Handles the specified (incoming) message using handler bindings.
@@ -78,7 +82,7 @@
             $log.error('Message.data is not valid JSON', msgEvent.data, e);
             return;
         }
-        $log.debug(' *Rx* >> ', ev.event, ev.payload);
+        $log.debug(' << *Rx* ', ev.event, ev.payload);
 
         if (h = handlers[ev.event]) {
             try {
@@ -129,12 +133,17 @@
         return ip;
     }
 
+    function informListeners(host, url) {
+        angular.forEach(openListeners, function(lsnr) {
+            lsnr.cb(host, url);
+        });
+    }
+
     function _send(ev) {
         $log.debug(' *Tx* >> ', ev.event, ev.payload);
         ws.send(JSON.stringify(ev));
     }
 
-
     // ===================
     // === API Functions
 
@@ -145,12 +154,14 @@
 
     // Currently supported opts:
     //   wsport: web socket port (other than default 8181)
-    // server: if defined, is the server address to use
-    function createWebSocket(opts, server) {
+    // host: if defined, is the host address to use
+    function createWebSocket(opts, _host_) {
         var wsport = (opts && opts.wsport) || null;
+
         webSockOpts = opts; // preserved for future calls
 
-        url = ufs.wsUrl('core', wsport, server);
+        host = _host_ || $loc.host();
+        url = ufs.wsUrl('core', wsport, _host_);
 
         $log.debug('Attempting to open websocket to: ' + url);
         ws = wsock.newWebSocket(url);
@@ -163,15 +174,18 @@
         return url;
     }
 
-    // Binds the specified message handlers.
-    //   keys are the event IDs
-    //   values are the API on which the handler function is a property
+    // Binds the message handlers to their message type (event type) as
+    //  specified in the given map. Note that keys are the event IDs; values
+    //  are either:
+    //     * the event handler function, or
+    //     * an API object which has an event handler for the key
+    //
     function bindHandlers(handlerMap) {
         var m = d3.map(handlerMap),
             dups = [];
 
         m.forEach(function (eventId, api) {
-            var fn = fs.isF(api[eventId]);
+            var fn = fs.isF(api) || fs.isF(api[eventId]);
             if (!fn) {
                 $log.warn(eventId + ' handler not a function');
                 return;
@@ -198,6 +212,28 @@
         });
     }
 
+    function addOpenListener(callback) {
+        var id = nextListenerId++,
+            cb = fs.isF(callback),
+            o = { id: id, cb: cb };
+
+        if (cb) {
+            openListeners[id] = o;
+        } else {
+            $log.error('WSS.addOpenListener(): callback not a function');
+            o.error = 'No callback defined';
+        }
+        return o;
+    }
+
+    function removeOpenListener(lsnr) {
+        var id = lsnr && lsnr.id,
+            o = openListeners[id];
+        if (o) {
+            delete openListeners[id];
+        }
+    }
+
     // Formulates an event message and sends it via the web-socket.
     //  If the websocket is not up yet, we store it in a pending list.
     function sendEvent(evType, payload) {
@@ -230,17 +266,15 @@
             wsock = _wsock_;
             vs = _vs_;
 
-            // TODO: Consider how to simplify handler structure
-            // Now it is an object of key -> object that has a method named 'key'.
-            bindHandlers({
-                bootstrap: builtinHandlers
-            });
+            bindHandlers(builtinHandlers);
 
             return {
                 resetSid: resetSid,
                 createWebSocket: createWebSocket,
                 bindHandlers: bindHandlers,
                 unbindHandlers: unbindHandlers,
+                addOpenListener: addOpenListener,
+                removeOpenListener: removeOpenListener,
                 sendEvent: sendEvent
             };
         }
diff --git a/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
index c4d92c2..7289e9d 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoEvent.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -30,7 +30,8 @@
     var $log, wss, tps, tis, tfs, tss, tts;
 
     // internal state
-    var handlerMap;
+    var handlerMap,
+        openListener;
 
     // ==========================
 
@@ -58,6 +59,14 @@
         };
     }
 
+    function wsOpen(host, url) {
+        $log.debug('TOPO: web socket open - cluster node:', host, 'URL:', url);
+
+        // TODO: request "instanceUpdate" events for all instances
+        // this should give us the updated uiAttached icon placement
+
+    }
+
     angular.module('ovTopo')
     .factory('TopoEventService',
         ['$log', '$location', 'WebSocketService',
@@ -76,6 +85,7 @@
             createHandlerMap();
 
             function start() {
+                openListener = wss.addOpenListener(wsOpen);
                 wss.bindHandlers(handlerMap);
                 wss.sendEvent('topoStart');
                 wss.sendEvent('requestSummary');
@@ -85,6 +95,8 @@
             function stop() {
                 wss.sendEvent('topoStop');
                 wss.unbindHandlers(handlerMap);
+                wss.removeOpenListener(openListener);
+                openListener = null;
                 $log.debug('topo comms stopped');
             }
 
diff --git a/web/gui/src/main/webapp/tests/app/fw/remote/websocket-spec.js b/web/gui/src/main/webapp/tests/app/fw/remote/websocket-spec.js
index 6faedc9..fc1e18d 100644
--- a/web/gui/src/main/webapp/tests/app/fw/remote/websocket-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/remote/websocket-spec.js
@@ -46,7 +46,7 @@
     it('should define api functions', function () {
         expect(fs.areFunctions(wss, [
             'resetSid', 'createWebSocket', 'bindHandlers', 'unbindHandlers',
-            'sendEvent'
+            'addOpenListener', 'removeOpenListener', 'sendEvent'
         ])).toBeTruthy();
     });