GUI -- Further work on refactoring Topology View server side code.
- includes some cleanup of UiMessageHandler and subclasses thereof.

Change-Id: Ie48d830447a4abe1b3accda41a934530a4d55d0e
diff --git a/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfUi.java b/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfUi.java
index 7118ee1..d9c9dad 100644
--- a/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfUi.java
+++ b/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfUi.java
@@ -108,7 +108,7 @@
         private boolean streamingEnabled = false;
 
         @Override
-        protected Collection<RequestHandler> getHandlers() {
+        protected Collection<RequestHandler> createRequestHandlers() {
             return ImmutableSet.of(
                     new IntentPerfStart(),
                     new IntentPerfStop()
@@ -135,8 +135,8 @@
 
 
         private ObjectNode sampleNode(Sample sample) {
-            ObjectNode sampleNode = mapper.createObjectNode();
-            ArrayNode an = mapper.createArrayNode();
+            ObjectNode sampleNode = objectNode();
+            ArrayNode an = arrayNode();
             sampleNode.put("time", sample.time);
             sampleNode.set("data", an);
 
diff --git a/core/api/src/main/java/org/onosproject/ui/RequestHandler.java b/core/api/src/main/java/org/onosproject/ui/RequestHandler.java
index efc9e93..1678923 100644
--- a/core/api/src/main/java/org/onosproject/ui/RequestHandler.java
+++ b/core/api/src/main/java/org/onosproject/ui/RequestHandler.java
@@ -83,6 +83,7 @@
      * @param sid       message sequence identifier
      * @param payload   message payload
      */
+    // TODO: remove sid from signature
     protected void sendMessage(String eventType, long sid, ObjectNode payload) {
         parent.connection().sendMessage(eventType, sid, payload);
     }
@@ -107,6 +108,7 @@
      * @param sid       sequence identifier
      * @param payload   message payload
      */
+    // TODO: remove sid from signature
     protected void chain(String eventType, long sid, ObjectNode payload) {
         parent.exec(eventType, sid, payload);
     }
@@ -114,11 +116,25 @@
     // ===================================================================
 
 
-    // FIXME : Javadocs
+    /**
+     * Returns the specified node property as a string.
+     *
+     * @param node message event
+     * @param key property name
+     * @return property as a string
+     */
     protected String string(ObjectNode node, String key) {
         return JsonUtils.string(node, key);
     }
 
+    /**
+     * Returns the specified node property as a string, with a default fallback.
+     *
+     * @param node         object node
+     * @param key          property name
+     * @param defValue     fallback value if property is absent
+     * @return property as a string
+     */
     protected String string(ObjectNode node, String key, String defValue) {
         return JsonUtils.string(node, key, defValue);
     }
diff --git a/core/api/src/main/java/org/onosproject/ui/UiConnection.java b/core/api/src/main/java/org/onosproject/ui/UiConnection.java
index 123acb9..ead7b0d 100644
--- a/core/api/src/main/java/org/onosproject/ui/UiConnection.java
+++ b/core/api/src/main/java/org/onosproject/ui/UiConnection.java
@@ -36,6 +36,7 @@
      * @param sid     message sequence number
      * @param payload message payload
      */
+    // TODO: remove sid parameter
     void sendMessage(String type, long sid, ObjectNode payload);
 
 }
\ No newline at end of file
diff --git a/core/api/src/main/java/org/onosproject/ui/UiMessageHandler.java b/core/api/src/main/java/org/onosproject/ui/UiMessageHandler.java
index e2f5f10..b3782a2 100644
--- a/core/api/src/main/java/org/onosproject/ui/UiMessageHandler.java
+++ b/core/api/src/main/java/org/onosproject/ui/UiMessageHandler.java
@@ -16,6 +16,7 @@
 package org.onosproject.ui;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.onlab.osgi.ServiceDirectory;
 import org.slf4j.Logger;
@@ -31,41 +32,47 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
- * Abstraction of an entity capable of processing a JSON message from the user
+ * Abstraction of an entity capable of processing JSON messages from the user
  * interface client.
  * <p>
- * The message is a JSON object with the following structure:
+ * The message structure is:
  * </p>
  * <pre>
  * {
  *     "type": "<em>event-type</em>",
- *     "sid": "<em>sequence-number</em>",
  *     "payload": {
  *         <em>arbitrary JSON object structure</em>
  *     }
  * }
  * </pre>
+ * On {@link #init initialization} the handler will create and cache
+ * {@link RequestHandler} instances, each of which are bound to a particular
+ * <em>event-type</em>. On {@link #process arrival} of a new message,
+ * the <em>event-type</em> is determined, and the message dispatched to the
+ * corresponding <em>RequestHandler</em>'s
+ * {@link RequestHandler#process process} method.
  */
 public abstract class UiMessageHandler {
 
     private final Logger log = LoggerFactory.getLogger(getClass());
     private final Map<String, RequestHandler> handlerMap = new HashMap<>();
+    private final ObjectMapper mapper = new ObjectMapper();
 
     private UiConnection connection;
     private ServiceDirectory directory;
 
-    /**
-     * Mapper for creating ObjectNodes and ArrayNodes etc.
-     */
-    protected final ObjectMapper mapper = new ObjectMapper();
 
     /**
-     * Subclasses must return the collection of handlers for the
-     * message types they handle.
+     * Subclasses must create and return the collection of request handlers
+     * for the message types they handle.
+     * <p>
+     * Note that request handlers should be stateless. When we are
+     * {@link #destroy destroyed}, we will simply drop our references to them
+     * and allow them to be garbage collected.
      *
      * @return the message handler instances
      */
-    protected abstract Collection<RequestHandler> getHandlers();
+    protected abstract Collection<RequestHandler> createRequestHandlers();
 
     /**
      * Returns the set of message types which this handler is capable of
@@ -84,10 +91,9 @@
      */
     public void process(ObjectNode message) {
         String type = JsonUtils.eventType(message);
-        // TODO: remove sid
-        long sid = JsonUtils.sid(message);
         ObjectNode payload = JsonUtils.payload(message);
-        exec(type, sid, payload);
+        // TODO: remove sid
+        exec(type, 0, payload);
     }
 
     /**
@@ -99,21 +105,10 @@
      */
     // TODO: remove sid from signature
     void exec(String eventType, long sid, ObjectNode payload) {
-        RequestHandler handler = handlerMap.get(eventType);
-        if (handler != null) {
+        RequestHandler requestHandler = handlerMap.get(eventType);
+        if (requestHandler != null) {
             log.debug("process {} event...", eventType);
-            handler.process(sid, payload);
-        }
-    }
-
-    private void bindHandlers() {
-        Collection<RequestHandler> handlers = getHandlers();
-        checkNotNull(handlers, "Handlers cannot be null");
-        checkArgument(!handlers.isEmpty(), "Handlers cannot be empty");
-
-        for (RequestHandler h : handlers) {
-            h.setParent(this);
-            handlerMap.put(h.eventType(), h);
+            requestHandler.process(sid, payload);
         }
     }
 
@@ -127,7 +122,15 @@
     public void init(UiConnection connection, ServiceDirectory directory) {
         this.connection = connection;
         this.directory = directory;
-        bindHandlers();
+
+        Collection<RequestHandler> handlers = createRequestHandlers();
+        checkNotNull(handlers, "Handlers cannot be null");
+        checkArgument(!handlers.isEmpty(), "Handlers cannot be empty");
+
+        for (RequestHandler h : handlers) {
+            h.setParent(this);
+            handlerMap.put(h.eventType(), h);
+        }
     }
 
     /**
@@ -136,6 +139,7 @@
     public void destroy() {
         this.connection = null;
         this.directory = null;
+        handlerMap.clear();
     }
 
     /**
@@ -168,4 +172,21 @@
         return directory.get(serviceClass);
     }
 
+    /**
+     * Returns a freshly minted object node.
+     *
+     * @return new object node
+     */
+    protected ObjectNode objectNode() {
+        return mapper.createObjectNode();
+    }
+
+    /**
+     * Returns a freshly minted array node.
+     *
+     * @return new array node
+     */
+    protected ArrayNode arrayNode() {
+        return mapper.createArrayNode();
+    }
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/AltTopoViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/AltTopoViewMessageHandler.java
index 8a7534f..fc16133 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/AltTopoViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/AltTopoViewMessageHandler.java
@@ -53,6 +53,7 @@
     private static final String TOPO_START = "topoStart";
     private static final String TOPO_STOP = "topoStop";
     private static final String REQ_SUMMARY = "requestSummary";
+    private static final String CANCEL_SUMMARY = "cancelSummary";
 
     private final Logger log = LoggerFactory.getLogger(getClass());
 
@@ -80,6 +81,13 @@
         currentSummaryGenerator = defaultSummaryGenerator;
     }
 
+    @Override
+    public void destroy() {
+//        cancelAllMonitoring();
+//        stopListeningToModel();
+        super.destroy();
+    }
+
 
     private String getVersion() {
         String ver = directory.get(CoreService.class).version().toString();
@@ -88,11 +96,12 @@
 
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(
                 new TopoStart(),
                 new TopoStop(),
-                new ReqSummary()
+                new ReqSummary(),
+                new CancelSummary()
                 // TODO: add more handlers here.....
         );
     }
@@ -157,6 +166,17 @@
         }
     }
 
+    private final class CancelSummary extends RequestHandler {
+        private CancelSummary() {
+            super(CANCEL_SUMMARY);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            modelService.stopSummaryMonitoring();
+        }
+    }
+
     // =====================================================================
 
     private final class DefSummaryGenerator extends AbstractSummaryGenerator {
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationViewMessageHandler.java
index 61a543a..77a5897 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationViewMessageHandler.java
@@ -57,7 +57,7 @@
     };
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(
                 new AppDataRequest(),
                 new AppMgmtRequest()
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java
index 6ae987e..da0eae0 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java
@@ -54,7 +54,7 @@
     private static final String ICON_ID_OFFLINE = "inactive";
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(new ClusterDataRequest());
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java
index 01ec738..d1cbcb4 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java
@@ -84,7 +84,7 @@
     private static final String ICON_ID_OFFLINE = "inactive";
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(
                 new DataRequestHandler(),
                 new DetailRequestHandler()
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/FlowViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/FlowViewMessageHandler.java
index ae01890..0217911 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/FlowViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/FlowViewMessageHandler.java
@@ -65,7 +65,7 @@
     };
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(new FlowDataRequest());
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/GroupViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/GroupViewMessageHandler.java
index 105a99f..d7e1047 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/GroupViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/GroupViewMessageHandler.java
@@ -56,7 +56,7 @@
     };
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(new GroupDataRequest());
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/HostViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/HostViewMessageHandler.java
index a42ed90..12a2b66 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/HostViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/HostViewMessageHandler.java
@@ -56,7 +56,7 @@
     };
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(new HostDataRequest());
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/IntentViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/IntentViewMessageHandler.java
index 29ec648..8ac51dd 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/IntentViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/IntentViewMessageHandler.java
@@ -63,7 +63,7 @@
     };
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(new IntentDataRequest());
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java
index f594a92..9d4608a 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java
@@ -59,7 +59,7 @@
     private static final String ICON_ID_OFFLINE = "inactive";
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(new LinkDataRequest());
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java
index dbb7032..92ebcf7 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java
@@ -54,7 +54,7 @@
     };
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(new PortDataRequest());
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
index 232393c..478bfe7 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
@@ -167,7 +167,7 @@
     }
 
     @Override
-    protected Collection<RequestHandler> getHandlers() {
+    protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(
                 new TopoStart(),
                 new TopoStop(),
@@ -254,8 +254,8 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            ObjectNode root = mapper.createObjectNode();
-            ArrayNode names = mapper.createArrayNode();
+            ObjectNode root = objectNode();
+            ArrayNode names = arrayNode();
             get(SpriteService.class).getNames().forEach(names::add);
             root.set("names", names);
             sendMessage("spriteListResponse", sid, root);
@@ -270,7 +270,7 @@
         @Override
         public void process(long sid, ObjectNode payload) {
             String name = string(payload, "name");
-            ObjectNode root = mapper.createObjectNode();
+            ObjectNode root = objectNode();
             root.set("data", get(SpriteService.class).get(name));
             sendMessage("spriteDataResponse", sid, root);
         }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
index 9f24f39..f87ad7e 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
@@ -171,7 +171,7 @@
 
     // Produces JSON structure from annotations.
     private JsonNode props(Annotations annotations) {
-        ObjectNode props = mapper.createObjectNode();
+        ObjectNode props = objectNode();
         if (annotations != null) {
             for (String key : annotations.keys()) {
                 props.put(key, annotations.value(key));
@@ -197,7 +197,7 @@
 
     // Produces a log message event bound to the client.
     private ObjectNode message(String severity, long id, String message) {
-        ObjectNode payload = mapper.createObjectNode()
+        ObjectNode payload = objectNode()
                 .put("severity", severity)
                 .put("message", message);
 
@@ -266,14 +266,14 @@
     protected ObjectNode instanceMessage(ClusterEvent event, String messageType) {
         ControllerNode node = event.subject();
         int switchCount = mastershipService.getDevicesOf(node.id()).size();
-        ObjectNode payload = mapper.createObjectNode()
+        ObjectNode payload = objectNode()
                 .put("id", node.id().toString())
                 .put("ip", node.ip().toString())
                 .put("online", clusterService.getState(node.id()) == ACTIVE)
                 .put("uiAttached", node.equals(clusterService.getLocalNode()))
                 .put("switches", switchCount);
 
-        ArrayNode labels = mapper.createArrayNode();
+        ArrayNode labels = arrayNode();
         labels.add(node.id().toString());
         labels.add(node.ip().toString());
 
@@ -291,7 +291,7 @@
     // Produces a device event message to the client.
     protected ObjectNode deviceMessage(DeviceEvent event) {
         Device device = event.subject();
-        ObjectNode payload = mapper.createObjectNode()
+        ObjectNode payload = objectNode()
                 .put("id", device.id().toString())
                 .put("type", device.type().toString().toLowerCase())
                 .put("online", deviceService.isAvailable(device.id()))
@@ -299,7 +299,7 @@
 
         // Generate labels: id, chassis id, no-label, optional-name
         String name = device.annotations().value(AnnotationKeys.NAME);
-        ArrayNode labels = mapper.createArrayNode();
+        ArrayNode labels = arrayNode();
         labels.add("");
         labels.add(isNullOrEmpty(name) ? device.id().toString() : name);
         labels.add(device.id().toString());
@@ -318,7 +318,7 @@
     // Produces a link event message to the client.
     protected ObjectNode linkMessage(LinkEvent event) {
         Link link = event.subject();
-        ObjectNode payload = mapper.createObjectNode()
+        ObjectNode payload = objectNode()
                 .put("id", compactLinkString(link))
                 .put("type", link.type().toString().toLowerCase())
                 .put("online", link.state() == Link.State.ACTIVE)
@@ -336,13 +336,13 @@
     protected ObjectNode hostMessage(HostEvent event) {
         Host host = event.subject();
         String hostType = host.annotations().value(AnnotationKeys.TYPE);
-        ObjectNode payload = mapper.createObjectNode()
+        ObjectNode payload = objectNode()
                 .put("id", host.id().toString())
                 .put("type", isNullOrEmpty(hostType) ? "endstation" : hostType)
                 .put("ingress", compactLinkString(edgeLink(host, true)))
                 .put("egress", compactLinkString(edgeLink(host, false)));
-        payload.set("cp", hostConnect(mapper, host.location()));
-        payload.set("labels", labels(mapper, ip(host.ipAddresses()),
+        payload.set("cp", hostConnect(host.location()));
+        payload.set("labels", labels(ip(host.ipAddresses()),
                                      host.mac().toString()));
         payload.set("props", props(host.annotations()));
         addGeoLocation(host, payload);
@@ -354,15 +354,15 @@
     }
 
     // Encodes the specified host location into a JSON object.
-    private ObjectNode hostConnect(ObjectMapper mapper, HostLocation location) {
-        return mapper.createObjectNode()
+    private ObjectNode hostConnect(HostLocation location) {
+        return objectNode()
                 .put("device", location.deviceId().toString())
                 .put("port", location.port().toLong());
     }
 
     // Encodes the specified list of labels a JSON array.
-    private ArrayNode labels(ObjectMapper mapper, String... labels) {
-        ArrayNode json = mapper.createArrayNode();
+    private ArrayNode labels(String... labels) {
+        ArrayNode json = arrayNode();
         for (String label : labels) {
             json.add(label);
         }
@@ -402,7 +402,7 @@
             if (slat != null && slng != null && !slat.isEmpty() && !slng.isEmpty()) {
                 double lat = Double.parseDouble(slat);
                 double lng = Double.parseDouble(slng);
-                ObjectNode loc = mapper.createObjectNode()
+                ObjectNode loc = objectNode()
                         .put("type", "latlng").put("lat", lat).put("lng", lng);
                 payload.set("location", loc);
             }
@@ -531,22 +531,22 @@
 
     // Produces JSON message to trigger traffic overview visualization
     protected ObjectNode trafficSummaryMessage() {
-        ObjectNode payload = mapper.createObjectNode();
-        ArrayNode paths = mapper.createArrayNode();
+        ObjectNode payload = objectNode();
+        ArrayNode paths = arrayNode();
         payload.set("paths", paths);
 
-        ObjectNode pathNodeN = mapper.createObjectNode();
-        ArrayNode linksNodeN = mapper.createArrayNode();
-        ArrayNode labelsN = mapper.createArrayNode();
+        ObjectNode pathNodeN = objectNode();
+        ArrayNode linksNodeN = arrayNode();
+        ArrayNode labelsN = arrayNode();
 
         pathNodeN.put("class", "plain").put("traffic", false);
         pathNodeN.set("links", linksNodeN);
         pathNodeN.set("labels", labelsN);
         paths.add(pathNodeN);
 
-        ObjectNode pathNodeT = mapper.createObjectNode();
-        ArrayNode linksNodeT = mapper.createArrayNode();
-        ArrayNode labelsT = mapper.createArrayNode();
+        ObjectNode pathNodeT = objectNode();
+        ArrayNode linksNodeT = arrayNode();
+        ArrayNode labelsT = arrayNode();
 
         pathNodeT.put("class", "secondary").put("traffic", true);
         pathNodeT.set("links", linksNodeT);
@@ -581,8 +581,8 @@
 
     // Produces JSON message to trigger flow overview visualization
     protected ObjectNode flowSummaryMessage(Set<Device> devices) {
-        ObjectNode payload = mapper.createObjectNode();
-        ArrayNode paths = mapper.createArrayNode();
+        ObjectNode payload = objectNode();
+        ArrayNode paths = arrayNode();
         payload.set("paths", paths);
 
         for (Device device : devices) {
@@ -595,9 +595,9 @@
     }
 
     private void addLinkFlows(Link link, ArrayNode paths, Integer count) {
-        ObjectNode pathNode = mapper.createObjectNode();
-        ArrayNode linksNode = mapper.createArrayNode();
-        ArrayNode labels = mapper.createArrayNode();
+        ObjectNode pathNode = objectNode();
+        ArrayNode linksNode = arrayNode();
+        ArrayNode labels = arrayNode();
         boolean noFlows = count == null || count == 0;
         pathNode.put("class", noFlows ? "secondary" : "primary");
         pathNode.put("traffic", false);
@@ -610,8 +610,8 @@
 
     // Produces JSON message to trigger traffic visualization
     protected ObjectNode trafficMessage(TrafficClass... trafficClasses) {
-        ObjectNode payload = mapper.createObjectNode();
-        ArrayNode paths = mapper.createArrayNode();
+        ObjectNode payload = objectNode();
+        ArrayNode paths = arrayNode();
         payload.set("paths", paths);
 
         // Classify links based on their traffic traffic first...
@@ -624,10 +624,10 @@
             String tc = (biLink.classes() + (hasTraffic ? " animated" : "")).trim();
             ObjectNode pathNode = pathNodes.get(tc);
             if (pathNode == null) {
-                pathNode = mapper.createObjectNode()
+                pathNode = objectNode()
                         .put("class", tc).put("traffic", hasTraffic);
-                pathNode.set("links", mapper.createArrayNode());
-                pathNode.set("labels", mapper.createArrayNode());
+                pathNode.set("links", arrayNode());
+                pathNode.set("labels", arrayNode());
                 pathNodes.put(tc, pathNode);
                 paths.add(pathNode);
             }
@@ -701,11 +701,11 @@
     // connectivity intent
     protected void addPathTraffic(ArrayNode paths, String type, String trafficType,
                                   Iterable<Link> links) {
-        ObjectNode pathNode = mapper.createObjectNode();
-        ArrayNode linksNode = mapper.createArrayNode();
+        ObjectNode pathNode = objectNode();
+        ArrayNode linksNode = arrayNode();
 
         if (links != null) {
-            ArrayNode labels = mapper.createArrayNode();
+            ArrayNode labels = arrayNode();
             boolean hasTraffic = false;
             for (Link link : links) {
                 if (isInfrastructureEgress(link)) {
@@ -767,10 +767,10 @@
     // Produces JSON property details.
     private ObjectNode json(String id, String type, Prop... props) {
         ObjectMapper mapper = new ObjectMapper();
-        ObjectNode result = mapper.createObjectNode()
+        ObjectNode result = objectNode()
                 .put("id", id).put("type", type);
-        ObjectNode pnode = mapper.createObjectNode();
-        ArrayNode porder = mapper.createArrayNode();
+        ObjectNode pnode = objectNode();
+        ArrayNode porder = arrayNode();
         for (Prop p : props) {
             porder.add(p.key);
             pnode.put(p.key, p.value);
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
index 3295b9e..c571f8f 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
@@ -41,7 +41,7 @@
 
     private static final Logger log = LoggerFactory.getLogger(UiWebSocket.class);
 
-    private static final long MAX_AGE_MS = 15000;
+    private static final long MAX_AGE_MS = 15_000;
 
     private static final byte PING = 0x9;
     private static final byte PONG = 0xA;
@@ -83,8 +83,10 @@
      * @return true if idle or closed
      */
     synchronized boolean isIdle() {
-        boolean idle = (System.currentTimeMillis() - lastActive) > MAX_AGE_MS;
+        long quietFor = System.currentTimeMillis() - lastActive;
+        boolean idle = quietFor > MAX_AGE_MS;
         if (idle || (connection != null && !connection.isOpen())) {
+            log.debug("IDLE (or closed) websocket [{} ms]", quietFor);
             return true;
         } else if (connection != null) {
             try {
@@ -108,7 +110,8 @@
     @Override
     public synchronized void onClose(int closeCode, String message) {
         destroyHandlers();
-        log.info("GUI client disconnected");
+        log.info("GUI client disconnected [close-code={}, message={}]",
+                 closeCode, message);
     }
 
     @Override
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoUiModelManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoUiModelManager.java
index 7cb0e40..3a3bf68 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoUiModelManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoUiModelManager.java
@@ -50,6 +50,8 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
 
 import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
 import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
@@ -67,6 +69,9 @@
 @Service
 public class TopoUiModelManager implements TopoUiModelService {
 
+    // TODO: put back to 30,000 ms for production
+    private static final long SUMMARY_PERIOD = 15_000;
+
     private final Logger log = LoggerFactory.getLogger(getClass());
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -105,6 +110,11 @@
     private final TopoMessageFactory messageFactory = new TopoMessageFactory();
     private final MetaDb metaDb = new MetaDb();
 
+    private final Timer timer = new Timer("topology-view");
+
+    private TimerTask summaryTask = null;
+    private boolean summaryRunning = false;
+
 
     @Activate
     public void activate() {
@@ -135,7 +145,8 @@
     //  causes the listener (for an AltTopoViewMessageHandler instance) to
     //  be removed.
     // ==== Somehow need to tie this in to the GUI-disconnected event.
-
+    //  This probably requires client-generated heartbeat messages to
+    //  Keep the connection alive.
 
 
     @Override
@@ -159,16 +170,32 @@
     }
 
     @Override
-    public void startSummaryMonitoring() {
-        // TODO: set up periodic monitoring task
-        // send a summary now, and periodically...
-        post(new TopoUiEvent(SUMMARY_UPDATE, null));
+    public synchronized void startSummaryMonitoring() {
+        // first, cancel previous task if not canceled already
+        stopSummaryMonitoring();
+
+        // create and start a summary task, to execute with no delay, and
+        // every SUMMARY_PERIOD milliseconds thereafter.
+        summaryTask = new TimerTask() {
+            @Override
+            public void run() {
+                if (summaryRunning) {
+                    post(new TopoUiEvent(SUMMARY_UPDATE, null));
+                }
+            }
+        };
+
+        timer.schedule(summaryTask, 0, SUMMARY_PERIOD);
+        summaryRunning = true;
     }
 
     @Override
-    public void stopSummaryMonitoring() {
-        // TODO: cancel monitoring task
-
+    public synchronized void stopSummaryMonitoring() {
+        if (summaryTask != null) {
+            summaryTask.cancel();
+            summaryTask = null;
+        }
+        summaryRunning = false;
     }
 
     @Override