ONOS-4972: Augmented UiModelEvents to hold context and memo about the event subject.

Change-Id: Id0e28d8d5d3eb80fba36e0392cc80167effd39bc
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiDeviceLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiDeviceLink.java
index 3eaa827..75e2560 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiDeviceLink.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiDeviceLink.java
@@ -61,12 +61,12 @@
 
     @Override
     public String endPortA() {
-        return portA.toString();
+        return portA == null ? null : portA.toString();
     }
 
     @Override
     public String endPortB() {
-        return portB.toString();
+        return portB == null ? null : portB.toString();
     }
 
 
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 328207d..e1d403d 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
@@ -96,6 +96,8 @@
         UiSharedTopologyModel sharedModel = directory.get(UiSharedTopologyModel.class);
         UiTopoLayoutService layoutService = directory.get(UiTopoLayoutService.class);
 
+        sharedModel.injectJsonifier(t2json);
+
         topoSession = new UiTopoSession(this, t2json, sharedModel, layoutService);
 
         // FIXME: this is temporary to prevent unhandled events being set to GUI...
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
index ebc20fe..6234391 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
@@ -44,6 +44,7 @@
 import org.onosproject.ui.impl.topo.model.UiModelEvent;
 import org.onosproject.ui.model.topo.UiClusterMember;
 import org.onosproject.ui.model.topo.UiDevice;
+import org.onosproject.ui.model.topo.UiElement;
 import org.onosproject.ui.model.topo.UiHost;
 import org.onosproject.ui.model.topo.UiLink;
 import org.onosproject.ui.model.topo.UiNode;
@@ -82,6 +83,8 @@
     private static final String HOST = "host";
     private static final String TYPE = "type";
     private static final String SUBJECT = "subject";
+    private static final String DATA = "data";
+    private static final String MEMO = "memo";
 
     private final Logger log = LoggerFactory.getLogger(getClass());
 
@@ -267,6 +270,28 @@
     }
 
     /**
+     * Creates a JSON representation of a UI element.
+     *
+     * @param element the source element
+     * @return a JSON representation of that element
+     */
+    public ObjectNode jsonUiElement(UiElement element) {
+        if (element instanceof UiNode) {
+            return json((UiNode) element);
+        }
+        if (element instanceof UiLink) {
+            return json((UiLink) element);
+        }
+
+        // TODO: UiClusterMember
+
+        // Unrecognized UiElement class
+        return objectNode()
+                .put("warning", "unknown UiElement... cannot encode")
+                .put("javaclass", element.getClass().toString());
+    }
+
+    /**
      * Creates a JSON representation of a UI model event.
      *
      * @param modelEvent the source model event
@@ -276,6 +301,8 @@
         ObjectNode payload = objectNode();
         payload.put(TYPE, enumToString(modelEvent.type()));
         payload.put(SUBJECT, modelEvent.subject().idAsString());
+        payload.set(DATA, modelEvent.data());
+        payload.put(MEMO, modelEvent.memo());
         return payload;
     }
 
@@ -408,14 +435,17 @@
     }
 
     private ObjectNode json(UiSynthLink sLink) {
-        UiLink uLink = sLink.link();
+        return json(sLink.link());
+    }
+
+    private ObjectNode json(UiLink link) {
         ObjectNode data = objectNode()
-                .put("id", uLink.idAsString())
-                .put("epA", uLink.endPointA())
-                .put("epB", uLink.endPointB())
-                .put("type", uLink.type());
-        String pA = uLink.endPortA();
-        String pB = uLink.endPortB();
+                .put("id", link.idAsString())
+                .put("epA", link.endPointA())
+                .put("epB", link.endPointB())
+                .put("type", link.type());
+        String pA = link.endPortA();
+        String pB = link.endPortB();
         if (pA != null) {
             data.put("portA", pA);
         }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
index f98ab7b..24fda23 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
@@ -16,6 +16,7 @@
 
 package org.onosproject.ui.impl.topo.model;
 
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.onosproject.cluster.ControllerNode;
 import org.onosproject.cluster.NodeId;
 import org.onosproject.cluster.RoleInfo;
@@ -30,6 +31,7 @@
 import org.onosproject.net.region.Region;
 import org.onosproject.net.region.RegionId;
 import org.onosproject.ui.UiTopoLayoutService;
+import org.onosproject.ui.impl.topo.Topo2Jsonifier;
 import org.onosproject.ui.model.ServiceBundle;
 import org.onosproject.ui.model.topo.UiClusterMember;
 import org.onosproject.ui.model.topo.UiDevice;
@@ -70,6 +72,10 @@
 class ModelCache {
 
     private static final String E_NO_ELEMENT = "Tried to remove non-member {}: {}";
+    private static final String MEMO_ADDED = "added";
+    private static final String MEMO_UPDATED = "updated";
+    private static final String MEMO_REMOVED = "removed";
+    private static final String MEMO_MOVED = "moved";
 
     private static final Logger log = LoggerFactory.getLogger(ModelCache.class);
 
@@ -77,6 +83,8 @@
     private final EventDispatcher dispatcher;
     private final UiTopology uiTopology = new UiTopology();
 
+    private Topo2Jsonifier t2json;
+
     ModelCache(ServiceBundle services, EventDispatcher eventDispatcher) {
         this.services = services;
         this.dispatcher = eventDispatcher;
@@ -87,8 +95,13 @@
         return "ModelCache{" + uiTopology + "}";
     }
 
-    private void postEvent(UiModelEvent.Type type, UiElement subject) {
-        dispatcher.post(new UiModelEvent(type, subject));
+    private void postEvent(UiModelEvent.Type type, UiElement subject, String memo) {
+        ObjectNode data = t2json != null ? t2json.jsonUiElement(subject) : null;
+        dispatcher.post(new UiModelEvent(type, subject, data, memo));
+    }
+
+    void injectJsonifier(Topo2Jsonifier t2json) {
+        this.t2json = t2json;
     }
 
     void clear() {
@@ -131,13 +144,15 @@
     // invoked from UiSharedTopologyModel cluster event listener
     void addOrUpdateClusterMember(ControllerNode cnode) {
         NodeId id = cnode.id();
+        String memo = MEMO_UPDATED;
         UiClusterMember member = uiTopology.findClusterMember(id);
         if (member == null) {
             member = addNewClusterMember(cnode);
+            memo = MEMO_ADDED;
         }
         updateClusterMember(member);
 
-        postEvent(CLUSTER_MEMBER_ADDED_OR_UPDATED, member);
+        postEvent(CLUSTER_MEMBER_ADDED_OR_UPDATED, member, memo);
     }
 
     // package private for unit test access
@@ -151,7 +166,7 @@
         UiClusterMember member = uiTopology.findClusterMember(id);
         if (member != null) {
             uiTopology.remove(member);
-            postEvent(CLUSTER_MEMBER_REMOVED, member);
+            postEvent(CLUSTER_MEMBER_REMOVED, member, MEMO_REMOVED);
         } else {
             log.warn(E_NO_ELEMENT, "cluster node", id);
         }
@@ -256,13 +271,15 @@
     // invoked from UiSharedTopologyModel region listener
     void addOrUpdateRegion(Region region) {
         RegionId id = region.id();
+        String memo = MEMO_UPDATED;
         UiRegion uiRegion = uiTopology.findRegion(id);
         if (uiRegion == null) {
             uiRegion = addNewRegion(region);
+            memo = MEMO_ADDED;
         }
         updateRegion(uiRegion);
 
-        postEvent(REGION_ADDED_OR_UPDATED, uiRegion);
+        postEvent(REGION_ADDED_OR_UPDATED, uiRegion, memo);
     }
 
     // package private for unit test access
@@ -276,7 +293,7 @@
         UiRegion uiRegion = uiTopology.findRegion(id);
         if (uiRegion != null) {
             uiTopology.remove(uiRegion);
-            postEvent(REGION_REMOVED, uiRegion);
+            postEvent(REGION_REMOVED, uiRegion, MEMO_REMOVED);
         } else {
             log.warn(E_NO_ELEMENT, "region", id);
         }
@@ -313,14 +330,16 @@
     // invoked from UiSharedTopologyModel device listener
     void addOrUpdateDevice(Device device) {
         DeviceId id = device.id();
+        String memo = MEMO_UPDATED;
         UiDevice uiDevice = uiTopology.findDevice(id);
         if (uiDevice == null) {
             uiDevice = addNewDevice(device);
+            memo = MEMO_ADDED;
         } else {
             updateDevice(uiDevice);
         }
 
-        postEvent(DEVICE_ADDED_OR_UPDATED, uiDevice);
+        postEvent(DEVICE_ADDED_OR_UPDATED, uiDevice, memo);
     }
 
     // package private for unit test access
@@ -334,7 +353,7 @@
         UiDevice uiDevice = uiTopology.findDevice(id);
         if (uiDevice != null) {
             uiTopology.remove(uiDevice);
-            postEvent(DEVICE_REMOVED, uiDevice);
+            postEvent(DEVICE_REMOVED, uiDevice, MEMO_REMOVED);
         } else {
             log.warn(E_NO_ELEMENT, "device", id);
         }
@@ -378,13 +397,15 @@
     // invoked from UiSharedTopologyModel link listener
     void addOrUpdateDeviceLink(Link link) {
         UiLinkId id = uiLinkId(link);
+        String memo = MEMO_UPDATED;
         UiDeviceLink uiDeviceLink = uiTopology.findDeviceLink(id);
         if (uiDeviceLink == null) {
             uiDeviceLink = addNewDeviceLink(id);
+            memo = MEMO_ADDED;
         }
         updateDeviceLink(uiDeviceLink, link);
 
-        postEvent(LINK_ADDED_OR_UPDATED, uiDeviceLink);
+        postEvent(LINK_ADDED_OR_UPDATED, uiDeviceLink, memo);
     }
 
     // package private for unit test access
@@ -399,10 +420,10 @@
         if (uiDeviceLink != null) {
             boolean remaining = uiDeviceLink.detachBackingLink(link);
             if (remaining) {
-                postEvent(LINK_ADDED_OR_UPDATED, uiDeviceLink);
+                postEvent(LINK_ADDED_OR_UPDATED, uiDeviceLink, MEMO_UPDATED);
             } else {
                 uiTopology.remove(uiDeviceLink);
-                postEvent(LINK_REMOVED, uiDeviceLink);
+                postEvent(LINK_REMOVED, uiDeviceLink, MEMO_REMOVED);
             }
         } else {
             log.warn(E_NO_ELEMENT, "Device link", id);
@@ -474,13 +495,15 @@
     // invoked from UiSharedTopologyModel host listener
     void addOrUpdateHost(Host host) {
         HostId id = host.id();
+        String memo = MEMO_UPDATED;
         UiHost uiHost = uiTopology.findHost(id);
         if (uiHost == null) {
             uiHost = addNewHost(host);
+            memo = MEMO_ADDED;
         }
         updateHost(uiHost, host);
 
-        postEvent(HOST_ADDED_OR_UPDATED, uiHost);
+        postEvent(HOST_ADDED_OR_UPDATED, uiHost, memo);
     }
 
     // invoked from UiSharedTopologyModel host listener
@@ -488,7 +511,7 @@
         UiHost uiHost = uiTopology.findHost(prevHost.id());
         if (uiHost != null) {
             updateHost(uiHost, host);
-            postEvent(HOST_MOVED, uiHost);
+            postEvent(HOST_MOVED, uiHost, MEMO_MOVED);
         } else {
             log.warn(E_NO_ELEMENT, "host", prevHost.id());
         }
@@ -507,7 +530,7 @@
             UiEdgeLink edgeLink = uiTopology.findEdgeLink(uiHost.edgeLinkId());
             uiTopology.remove(edgeLink);
             uiTopology.remove(uiHost);
-            postEvent(HOST_REMOVED, uiHost);
+            postEvent(HOST_REMOVED, uiHost, MEMO_REMOVED);
         } else {
             log.warn(E_NO_ELEMENT, "host", id);
         }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
index 10a3523..4e2a15c 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
@@ -16,6 +16,7 @@
 
 package org.onosproject.ui.impl.topo.model;
 
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.onosproject.event.AbstractEvent;
 import org.onosproject.ui.model.topo.UiElement;
 
@@ -24,10 +25,9 @@
  */
 public class UiModelEvent extends AbstractEvent<UiModelEvent.Type, UiElement> {
 
-    protected UiModelEvent(Type type, UiElement subject) {
-        super(type, subject);
-    }
-
+    /**
+     * Enumeration of event types.
+     */
     enum Type {
         CLUSTER_MEMBER_ADDED_OR_UPDATED,
         CLUSTER_MEMBER_REMOVED,
@@ -45,4 +45,42 @@
         HOST_MOVED,
         HOST_REMOVED
     }
+
+    private final ObjectNode data;
+    private final String memo;
+
+    /**
+     * Creates a UI model event. Note that the memo field can be used to
+     * pass a hint to the listener about the event.
+     *
+     * @param type    event type
+     * @param subject subject of the event
+     * @param data    data containing details of the subject
+     * @param memo    a note about the event
+     */
+    protected UiModelEvent(Type type, UiElement subject, ObjectNode data,
+                           String memo) {
+        super(type, subject);
+        this.data = data;
+        this.memo = memo;
+    }
+
+    /**
+     * Returns the data of the subject.
+     *
+     * @return the subject data
+     */
+    public ObjectNode data() {
+        return data;
+    }
+
+    /**
+     * Returns the memo.
+     *
+     * @return the memo
+     */
+    public String memo() {
+        return memo;
+    }
+
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
index 2a66b8e..80d134a 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
@@ -22,7 +22,6 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
-import org.onlab.util.Tools;
 import org.onosproject.cluster.ClusterEvent;
 import org.onosproject.cluster.ClusterEventListener;
 import org.onosproject.cluster.ClusterService;
@@ -61,6 +60,7 @@
 import org.onosproject.net.statistic.StatisticService;
 import org.onosproject.net.topology.TopologyService;
 import org.onosproject.ui.UiTopoLayoutService;
+import org.onosproject.ui.impl.topo.Topo2Jsonifier;
 import org.onosproject.ui.impl.topo.UiTopoSession;
 import org.onosproject.ui.model.ServiceBundle;
 import org.onosproject.ui.model.topo.UiClusterMember;
@@ -75,7 +75,9 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.util.Tools.groupedThreads;
 
 /**
  * Service that creates and maintains the UI-model of the network topology.
@@ -143,7 +145,7 @@
     @Activate
     protected void activate() {
         cache = new ModelCache(new DefaultServiceBundle(), eventDispatcher);
-        eventHandler = Executors.newSingleThreadExecutor(Tools.groupedThreads("onos/ui/topo", "event-handler", log));
+        eventHandler = newSingleThreadExecutor(groupedThreads("onos/ui/topo", "event-handler", log));
 
         eventDispatcher.addSink(UiModelEvent.class, listenerRegistry);
 
@@ -182,6 +184,17 @@
         log.info("Stopped");
     }
 
+    /**
+     * Injects an instance of the JSON-ifier (which has been bound to the
+     * services (link, host, device, ...)) to be passed on to the Model Cache,
+     * for use in forming UiModelEvent payloads.
+     *
+     * @param t2json JSONifier
+     */
+    public void injectJsonifier(Topo2Jsonifier t2json) {
+        cache.injectJsonifier(t2json);
+    }
+
 
     /**
      * Registers a UI topology session with the topology model.