Added ping to age-out idle or stale connections.
Repushing due to a javadoc inherited from the master.

Change-Id: I375fc114079ba7d0a2897791ba05fa41762db80b
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/GuiWebSocketServlet.java b/web/gui/src/main/java/org/onlab/onos/gui/GuiWebSocketServlet.java
index 02b46ed..aa00a863 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/GuiWebSocketServlet.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/GuiWebSocketServlet.java
@@ -20,19 +20,56 @@
 import org.onlab.osgi.DefaultServiceDirectory;
 import org.onlab.osgi.ServiceDirectory;
 
+import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
 
 /**
  * Web socket servlet capable of creating various sockets for the user interface.
  */
 public class GuiWebSocketServlet extends WebSocketServlet {
 
+    private static final long PING_DELAY_MS = 5000;
+
     private ServiceDirectory directory = new DefaultServiceDirectory();
 
+    private final Set<TopologyWebSocket> sockets = new HashSet<>();
+    private final Timer timer = new Timer();
+    private final TimerTask pruner = new Pruner();
+
+    @Override
+    public void init() throws ServletException {
+        super.init();
+        timer.schedule(pruner, PING_DELAY_MS, PING_DELAY_MS);
+    }
+
     @Override
     public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
-
-        return new TopologyWebSocket(directory);
+        TopologyWebSocket socket = new TopologyWebSocket(directory);
+        synchronized (sockets) {
+            sockets.add(socket);
+        }
+        return socket;
     }
 
+    // Task for pruning web-sockets that are idle.
+    private class Pruner extends TimerTask {
+        @Override
+        public void run() {
+            synchronized (sockets) {
+                Iterator<TopologyWebSocket> it = sockets.iterator();
+                while (it.hasNext()) {
+                    TopologyWebSocket socket = it.next();
+                    if (socket.isIdle()) {
+                        it.remove();
+                        socket.close();
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
index 2506b13..e4bfccd 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
@@ -61,13 +61,21 @@
  * Web socket capable of interacting with the GUI topology view.
  */
 public class TopologyWebSocket
-        extends TopologyMessages implements WebSocket.OnTextMessage {
+        extends TopologyMessages
+        implements WebSocket.OnTextMessage, WebSocket.OnControl {
+
+    private static final long MAX_AGE_MS = 15000;
+
+    private static final byte PING = 0x9;
+    private static final byte PONG = 0xA;
+    private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad};
 
     private static final String APP_ID = "org.onlab.onos.gui";
 
     private final ApplicationId appId;
 
     private Connection connection;
+    private FrameConnection control;
 
     private final ClusterEventListener clusterListener = new InternalClusterListener();
     private final DeviceListener deviceListener = new InternalDeviceListener();
@@ -79,6 +87,8 @@
     // Intents that are being monitored for the GUI
     private static Map<IntentId, Long> intentsToMonitor = new ConcurrentHashMap<>();
 
+    private long lastActive = System.currentTimeMillis();
+
     /**
      * Creates a new web-socket for serving data to GUI topology view.
      *
@@ -89,9 +99,37 @@
         appId = directory.get(CoreService.class).registerApplication(APP_ID);
     }
 
+    /**
+     * Issues a close on the connection.
+     */
+    synchronized void close() {
+        if (connection.isOpen()) {
+            removeListeners();
+            connection.close();
+        }
+    }
+
+    /**
+     * Indicates if this connection is idle.
+     */
+    synchronized boolean isIdle() {
+        boolean idle = (System.currentTimeMillis() - lastActive) > MAX_AGE_MS;
+        if (idle || !connection.isOpen()) {
+            return true;
+        }
+        try {
+            control.sendControl(PING, PING_DATA, 0, PING_DATA.length);
+        } catch (IOException e) {
+            log.warn("Unable to send ping message due to: ", e);
+        }
+        return false;
+    }
+
     @Override
     public void onOpen(Connection connection) {
+        log.info("GUI client connected");
         this.connection = connection;
+        this.control = (FrameConnection) connection;
         addListeners();
 
         sendAllInstances();
@@ -101,12 +139,22 @@
     }
 
     @Override
-    public void onClose(int closeCode, String message) {
-        removeListeners();
+    public synchronized void onClose(int closeCode, String message) {
+        if (connection.isOpen()) {
+            removeListeners();
+        }
+        log.info("GUI client disconnected");
+    }
+
+    @Override
+    public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
+        lastActive = System.currentTimeMillis();
+        return true;
     }
 
     @Override
     public void onMessage(String data) {
+        lastActive = System.currentTimeMillis();
         try {
             ObjectNode event = (ObjectNode) mapper.reader().readTree(data);
             String type = string(event, "event", "unknown");
@@ -123,16 +171,17 @@
             }
         } catch (Exception e) {
             log.warn("Unable to parse GUI request {} due to {}", data, e);
-            e.printStackTrace();
         }
     }
 
     // Sends the specified data to the client.
-    private void sendMessage(ObjectNode data) {
+    private synchronized void sendMessage(ObjectNode data) {
         try {
-            connection.sendMessage(data.toString());
+            if (connection.isOpen()) {
+                connection.sendMessage(data.toString());
+            }
         } catch (IOException e) {
-            e.printStackTrace();
+            log.warn("Unable to send message {} to GUI due to {}", data, e);
         }
     }
 
@@ -229,6 +278,7 @@
         linkService.removeListener(linkListener);
         hostService.removeListener(hostListener);
         mastershipService.removeListener(mastershipListener);
+        intentService.removeListener(intentListener);
     }
 
     // Cluster event listener.
@@ -268,6 +318,7 @@
         @Override
         public void event(MastershipEvent event) {
             // TODO: Is DeviceEvent.Type.DEVICE_MASTERSHIP_CHANGED the same?
+
         }
     }