Added mechanism for apps to easily add their own custom link/node/host highlighting wihout having to create a new UI extensions

Change-Id: Iefa21d76190c60db79a4b07a8b22e301d29fe58e
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/SampleHighlighterFactory.java b/web/gui/src/main/java/org/onosproject/ui/impl/SampleHighlighterFactory.java
new file mode 100644
index 0000000..509d817
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/SampleHighlighterFactory.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.ui.impl;
+
+import org.onosproject.net.EdgeLink;
+import org.onosproject.net.Host;
+import org.onosproject.net.Link;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiTopoHighlighter;
+import org.onosproject.ui.UiTopoHighlighterFactory;
+import org.onosproject.ui.topo.BaseLink;
+import org.onosproject.ui.topo.BaseLinkMap;
+import org.onosproject.ui.topo.Highlights;
+import org.onosproject.ui.topo.HostHighlight;
+import org.onosproject.ui.topo.LinkHighlight;
+import org.onosproject.ui.topo.Mod;
+import org.onosproject.ui.topo.NodeBadge;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+
+import static org.onosproject.net.DefaultEdgeLink.createEdgeLinks;
+import static org.onosproject.ui.topo.NodeBadge.text;
+
+@Component(immediate = false)
+public class SampleHighlighterFactory {
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected UiExtensionService uiExtensionService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected LinkService linkService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected HostService hostService;
+
+    private UiTopoHighlighterFactory foo = () ->
+            new TestHighlighter("foo", new Mod("style=\"stroke: #0ff; stroke-width: 10px;\""));
+    private UiTopoHighlighterFactory bar = () ->
+            new TestHighlighter("bar", new Mod("style=\"stroke: #f0f; stroke-width: 4px; stroke-dasharray: 5 2;\""));
+
+    @Activate
+    protected void activate() {
+        uiExtensionService.register(foo);
+        uiExtensionService.register(bar);
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        uiExtensionService.unregister(foo);
+        uiExtensionService.unregister(bar);
+    }
+
+    private final class TestHighlighter implements UiTopoHighlighter {
+
+        private final String name;
+        private final Mod mod;
+
+        private TestHighlighter(String name, Mod mod) {
+            this.name = name;
+            this.mod = mod;
+        }
+
+        @Override
+        public String name() {
+            return name;
+        }
+
+        @Override
+        public Highlights createHighlights() {
+            Highlights highlights = new Highlights();
+            BaseLinkMap linkMap = new BaseLinkMap();
+
+            // Create a map of base bi-links from the set of active links first.
+            for (Link link : linkService.getActiveLinks()) {
+                linkMap.add(link);
+            }
+
+            for (Host host : hostService.getHosts()) {
+                for (EdgeLink link : createEdgeLinks(host, false)) {
+                    linkMap.add(link);
+                }
+
+                // Also add a host badge for kicks.
+                HostHighlight hostHighlight = new HostHighlight(host.id().toString());
+                hostHighlight.setBadge(text(NodeBadge.Status.WARN, name));
+                highlights.add(hostHighlight);
+            }
+
+            // Now scan through the links and annotate them with desired highlights
+            for (BaseLink link : linkMap.biLinks()) {
+                highlights.add(new LinkHighlight(link.linkId(), LinkHighlight.Flavor.PRIMARY_HIGHLIGHT)
+                                       .addMod(mod).setLabel(name + "-" + link.one().src().port()));
+            }
+
+            return highlights;
+        }
+    }
+
+}
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 455b0f3..cae4760 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
@@ -113,6 +113,7 @@
     private static final String REQ_SEL_INTENT_TRAFFIC = "requestSelectedIntentTraffic";
     private static final String SEL_INTENT = "selectIntent";
     private static final String REQ_ALL_TRAFFIC = "requestAllTraffic";
+    private static final String REQ_CUSTOM_TRAFFIC = "requestCustomTraffic";
     private static final String REQ_DEV_LINK_FLOWS = "requestDeviceLinkFlows";
     private static final String CANCEL_TRAFFIC = "cancelTraffic";
     private static final String REQ_SUMMARY = "requestSummary";
@@ -246,6 +247,7 @@
                 new RemoveIntents(),
 
                 new ReqAllTraffic(),
+                new ReqCustomTraffic(),
                 new ReqDevLinkFlows(),
                 new ReqRelatedIntents(),
                 new ReqNextIntent(),
@@ -623,6 +625,17 @@
         }
     }
 
+    private final class ReqCustomTraffic extends RequestHandler {
+        private ReqCustomTraffic() {
+            super(REQ_CUSTOM_TRAFFIC);
+        }
+
+        @Override
+        public void process(ObjectNode payload) {
+            traffic.monitor((int) number(payload, "index"));
+        }
+    }
+
     private NodeSelection makeNodeSelection(ObjectNode payload) {
         return new NodeSelection(payload, services.device(), services.host(),
                                  services.link());
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
index 9d9b3c3..b585ffc 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
@@ -151,6 +151,14 @@
     }
 
     @Override
+    protected void sendCustomTraffic() {
+        log.debug("sendCustomTraffic");
+        if (topoHighlighter != null) {
+            msgHandler.sendHighlights(topoHighlighter.createHighlights());
+        }
+    }
+
+    @Override
     protected void sendClearHighlights() {
         log.debug("sendClearHighlights");
         msgHandler.sendHighlights(new Highlights());
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorBase.java b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorBase.java
index 2f14ac4..ae3c909 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorBase.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorBase.java
@@ -38,6 +38,9 @@
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.Link;
 import org.onosproject.net.statistic.Load;
+import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiTopoHighlighter;
+import org.onosproject.ui.UiTopoHighlighterFactory;
 import org.onosproject.ui.impl.topo.TopoologyTrafficMessageHandlerAbstract;
 import org.onosproject.ui.impl.topo.util.IntentSelection;
 import org.onosproject.ui.impl.topo.util.ServicesBundle;
@@ -69,8 +72,7 @@
 import static org.onosproject.net.statistic.PortStatisticsService.MetricType.BYTES;
 import static org.onosproject.net.statistic.PortStatisticsService.MetricType.PACKETS;
 import static org.onosproject.net.DefaultEdgeLink.createEdgeLinks;
-import static org.onosproject.ui.impl.TrafficMonitorBase.Mode.IDLE;
-import static org.onosproject.ui.impl.TrafficMonitorBase.Mode.SELECTED_INTENT;
+import static org.onosproject.ui.impl.TrafficMonitorBase.Mode.*;
 
 /**
  * Base superclass for traffic monitor (both 'classic' and 'topo2' versions).
@@ -85,6 +87,7 @@
     protected IntentSelection selectedIntents = null;
     protected final TopoologyTrafficMessageHandlerAbstract msgHandler;
     protected NodeSelection selectedNodes = null;
+    protected UiTopoHighlighter topoHighlighter = null;
 
     protected void sendSelectedIntents() {
         log.debug("sendSelectedIntents: {}", selectedIntents);
@@ -285,7 +288,8 @@
         ALL_PORT_TRAFFIC_PKT_PS,
         DEV_LINK_FLOWS,
         RELATED_INTENTS,
-        SELECTED_INTENT
+        SELECTED_INTENT,
+        CUSTOM_TRAFFIC_MONITOR
     }
 
     /**
@@ -375,6 +379,22 @@
         }
     }
 
+
+    public synchronized void monitor(int index) {
+        mode = CUSTOM_TRAFFIC_MONITOR;
+        List<UiTopoHighlighterFactory> factories = services.get(UiExtensionService.class)
+                .getTopoHighlighterFactories();
+        if (factories.isEmpty()) {
+            return;
+        }
+
+        UiTopoHighlighterFactory factory = factories.get(index % factories.size());
+        topoHighlighter = factory.newTopoHighlighter();
+        clearSelection();
+        scheduleTask();
+        sendCustomTraffic();
+    }
+
     /**
      * Monitor for traffic data to be sent back to the web client, under
      * the given mode, using the given selection of devices and hosts.
@@ -474,6 +494,12 @@
     protected abstract void sendSelectedIntentTraffic();
 
     /**
+     * Subclass should compile and send appropriate highlights data showing
+     * custom traffic on links.
+     */
+    protected abstract void sendCustomTraffic();
+
+    /**
      * Subclass should send a "clear highlights" event.
      */
     protected abstract void sendClearHighlights();
@@ -712,6 +738,9 @@
                     case SELECTED_INTENT:
                         sendSelectedIntentTraffic();
                         break;
+                    case CUSTOM_TRAFFIC_MONITOR:
+                        sendCustomTraffic();
+                        break;
 
                     default:
                         // RELATED_INTENTS and IDLE modes should never invoke
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
index 7d5c324..73de4c1 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
@@ -49,6 +49,7 @@
 import org.onosproject.ui.UiSessionToken;
 import org.onosproject.ui.UiTokenService;
 import org.onosproject.ui.UiTopo2OverlayFactory;
+import org.onosproject.ui.UiTopoHighlighterFactory;
 import org.onosproject.ui.UiTopoMap;
 import org.onosproject.ui.UiTopoMapFactory;
 import org.onosproject.ui.UiTopoOverlayFactory;
@@ -140,6 +141,7 @@
     private final List<UiExtension> extensions = Lists.newArrayList();
 
     private final List<UiGlyph> glyphs = Lists.newArrayList();
+    private final List<UiTopoHighlighterFactory> highlighterFactories = Lists.newArrayList();
 
     // Map of views to extensions
     private final Map<String, UiExtension> views = Maps.newHashMap();
@@ -366,6 +368,22 @@
     }
 
     @Override
+    public synchronized void register(UiTopoHighlighterFactory factory) {
+        checkPermission(UI_WRITE);
+        if (!highlighterFactories.contains(factory)) {
+            highlighterFactories.add(factory);
+            UiWebSocketServlet.sendToAll(GUI_ADDED, null);
+        }
+    }
+
+    @Override
+    public synchronized void unregister(UiTopoHighlighterFactory factory) {
+        checkPermission(UI_WRITE);
+        highlighterFactories.remove(factory);
+        UiWebSocketServlet.sendToAll(GUI_REMOVED, null);
+    }
+
+    @Override
     public synchronized List<UiExtension> getExtensions() {
         checkPermission(UI_READ);
         return ImmutableList.copyOf(extensions);
@@ -378,6 +396,12 @@
     }
 
     @Override
+    public synchronized List<UiTopoHighlighterFactory> getTopoHighlighterFactories() {
+        checkPermission(UI_READ);
+        return ImmutableList.copyOf(highlighterFactories);
+    }
+
+    @Override
     public synchronized UiExtension getViewExtension(String viewId) {
         checkPermission(UI_READ);
         return views.get(viewId);
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Traffic2Monitor.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Traffic2Monitor.java
index 76b9dc5..6a41cf2 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Traffic2Monitor.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Traffic2Monitor.java
@@ -65,6 +65,10 @@
     }
 
     @Override
+    protected void sendCustomTraffic() {
+    }
+
+    @Override
     protected void sendAllPortTrafficBits() {
         log.debug("TOPO-2-TRAFFIC: sendAllPortTrafficBits");
         msgHandler.sendHighlights(trafficSummary(TrafficLink.StatsType.PORT_STATS));
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/ServicesBundle.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/ServicesBundle.java
index e58b0f2..1567718 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/ServicesBundle.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/ServicesBundle.java
@@ -37,6 +37,8 @@
  */
 public class ServicesBundle {
 
+    private ServiceDirectory directory;
+
     private ClusterService clusterService;
 
     private TopologyService topologyService;
@@ -60,6 +62,7 @@
      */
     public ServicesBundle(ServiceDirectory directory) {
         checkNotNull(directory, "Directory cannot be null");
+        this.directory = directory;
 
         clusterService = directory.get(ClusterService.class);
 
@@ -184,4 +187,15 @@
     public PortStatisticsService portStats() {
         return portStatsService;
     }
+
+    /**
+     * Returns the implementation of the specified service class.
+     *
+     * @param serviceClass service class
+     * @param <T>          class of service
+     * @return implementation of the service class
+     */
+    public <T> T get(Class<T> serviceClass) {
+        return directory.get(serviceClass);
+    }
 }