ONOS-1479 -- GUI - augmenting topology view for extensibility: WIP.
- Major refactoring of TopologyViewMessageHandler and related classes.

Change-Id: I920f7f9f7317f3987a9a8da35ac086e9f8cab8d3
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java
new file mode 100644
index 0000000..0826655
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 com.google.common.collect.ImmutableList;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.intent.FlowRuleIntent;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.LinkCollectionIntent;
+import org.onosproject.net.intent.OpticalConnectivityIntent;
+import org.onosproject.net.intent.OpticalPathIntent;
+import org.onosproject.net.intent.PathIntent;
+import org.onosproject.net.statistic.Load;
+import org.onosproject.ui.impl.topo.BiLink;
+import org.onosproject.ui.impl.topo.IntentSelection;
+import org.onosproject.ui.impl.topo.LinkStatsType;
+import org.onosproject.ui.impl.topo.NodeSelection;
+import org.onosproject.ui.impl.topo.ServicesBundle;
+import org.onosproject.ui.impl.topo.TopoUtils;
+import org.onosproject.ui.impl.topo.TopologyViewIntentFilter;
+import org.onosproject.ui.impl.topo.TrafficClass;
+import org.onosproject.ui.topo.Highlights;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
+import static org.onosproject.ui.impl.TrafficMonitorObject.Mode.IDLE;
+import static org.onosproject.ui.impl.TrafficMonitorObject.Mode.SEL_INTENT;
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.PRIMARY_HIGHLIGHT;
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.SECONDARY_HIGHLIGHT;
+
+/**
+ * Encapsulates the behavior of monitoring specific traffic patterns.
+ */
+public class TrafficMonitorObject {
+
+    // 4 Kilo Bytes as threshold
+    private static final double BPS_THRESHOLD = 4 * TopoUtils.KILO;
+
+    private static final Logger log =
+            LoggerFactory.getLogger(TrafficMonitorObject.class);
+
+    /**
+     * Designates the different modes of operation.
+     */
+    public enum Mode {
+        IDLE,
+        ALL_FLOW_TRAFFIC,
+        ALL_PORT_TRAFFIC,
+        DEV_LINK_FLOWS,
+        RELATED_INTENTS,
+        SEL_INTENT
+    }
+
+    private final long trafficPeriod;
+    private final ServicesBundle servicesBundle;
+    private final TopologyViewMessageHandler messageHandler;
+    private final TopologyViewIntentFilter intentFilter;
+
+    private final Timer timer = new Timer("topo-traffic");
+
+    private TimerTask trafficTask = null;
+    private Mode mode = IDLE;
+    private NodeSelection selectedNodes = null;
+    private IntentSelection selectedIntents = null;
+
+
+    /**
+     * Constructs a traffic monitor.
+     *
+     * @param trafficPeriod   traffic task period in ms
+     * @param servicesBundle  bundle of services
+     * @param messageHandler  our message handler
+     */
+    public TrafficMonitorObject(long trafficPeriod,
+                                ServicesBundle servicesBundle,
+                                TopologyViewMessageHandler messageHandler) {
+        this.trafficPeriod = trafficPeriod;
+        this.servicesBundle = servicesBundle;
+        this.messageHandler = messageHandler;
+
+        intentFilter = new TopologyViewIntentFilter(servicesBundle);
+    }
+
+    // =======================================================================
+    // === API === // TODO: add javadocs
+
+    public synchronized void monitor(Mode mode) {
+        log.debug("monitor: {}", mode);
+        this.mode = mode;
+
+        switch (mode) {
+            case ALL_FLOW_TRAFFIC:
+                clearSelection();
+                scheduleTask();
+                sendAllFlowTraffic();
+                break;
+
+            case ALL_PORT_TRAFFIC:
+                clearSelection();
+                scheduleTask();
+                sendAllPortTraffic();
+                break;
+
+            case SEL_INTENT:
+                scheduleTask();
+                sendSelectedIntentTraffic();
+                break;
+
+            default:
+                log.debug("Unexpected call to monitor({})", mode);
+                clearAll();
+                break;
+        }
+    }
+
+    public synchronized void monitor(Mode mode, NodeSelection nodeSelection) {
+        log.debug("monitor: {} -- {}", mode, nodeSelection);
+        this.mode = mode;
+        this.selectedNodes = nodeSelection;
+
+        switch (mode) {
+            case DEV_LINK_FLOWS:
+                // only care about devices (not hosts)
+                if (selectedNodes.devices().isEmpty()) {
+                    sendClearAll();
+                } else {
+                    scheduleTask();
+                    sendDeviceLinkFlows();
+                }
+                break;
+
+            case RELATED_INTENTS:
+                if (selectedNodes.none()) {
+                    sendClearAll();
+                } else {
+                    selectedIntents = new IntentSelection(selectedNodes, intentFilter);
+                    if (selectedIntents.none()) {
+                        sendClearAll();
+                    } else {
+                        sendSelectedIntents();
+                    }
+                }
+                break;
+
+            default:
+                log.debug("Unexpected call to monitor({}, {})", mode, nodeSelection);
+                clearAll();
+                break;
+        }
+    }
+
+    public synchronized void monitor(Intent intent) {
+        log.debug("monitor intent: {}", intent.id());
+        selectedNodes = null;
+        selectedIntents = new IntentSelection(intent);
+        mode = SEL_INTENT;
+        scheduleTask();
+        sendSelectedIntentTraffic();
+    }
+
+    public synchronized void selectNextIntent() {
+        if (selectedIntents != null) {
+            selectedIntents.next();
+            sendSelectedIntents();
+        }
+    }
+
+    public synchronized void selectPreviousIntent() {
+        if (selectedIntents != null) {
+            selectedIntents.prev();
+            sendSelectedIntents();
+        }
+    }
+
+    public synchronized void pokeIntent() {
+        if (mode == SEL_INTENT) {
+            sendSelectedIntentTraffic();
+        }
+    }
+
+    public synchronized void stop() {
+        log.debug("STOP");
+        if (mode != IDLE) {
+            sendClearAll();
+        }
+    }
+
+
+    // =======================================================================
+    // === Helper methods ===
+
+    private void sendClearAll() {
+        clearAll();
+        sendClearHighlights();
+    }
+
+    private void clearAll() {
+        this.mode = IDLE;
+        clearSelection();
+        cancelTask();
+    }
+
+    private void clearSelection() {
+        selectedNodes = null;
+        selectedIntents = null;
+    }
+
+    private synchronized void  scheduleTask() {
+        if (trafficTask == null) {
+            log.debug("Starting up background traffic task...");
+            trafficTask = new TrafficMonitor();
+            timer.schedule(trafficTask, trafficPeriod, trafficPeriod);
+        } else {
+            // TEMPORARY until we are sure this is working correctly
+            log.debug("(traffic task already running)");
+        }
+    }
+
+    private synchronized void cancelTask() {
+        if (trafficTask != null) {
+            trafficTask.cancel();
+            trafficTask = null;
+        }
+    }
+
+    // ---
+
+    private void sendAllFlowTraffic() {
+        log.debug("sendAllFlowTraffic");
+        sendHighlights(trafficSummary(LinkStatsType.FLOW_STATS));
+    }
+
+    private void sendAllPortTraffic() {
+        log.debug("sendAllPortTraffic");
+        sendHighlights(trafficSummary(LinkStatsType.PORT_STATS));
+    }
+
+    private void sendDeviceLinkFlows() {
+        log.debug("sendDeviceLinkFlows: {}", selectedNodes);
+        sendHighlights(deviceLinkFlows());
+    }
+
+    private void sendSelectedIntents() {
+        log.debug("sendSelectedIntents: {}", selectedIntents);
+        sendHighlights(intentGroup());
+    }
+
+    private void sendSelectedIntentTraffic() {
+        log.debug("sendSelectedIntentTraffic: {}", selectedIntents);
+        sendHighlights(intentTraffic());
+    }
+
+    private void sendClearHighlights() {
+        log.debug("sendClearHighlights");
+        sendHighlights(new Highlights());
+    }
+
+    private void sendHighlights(Highlights highlights) {
+        messageHandler.sendHighlights(highlights);
+    }
+
+
+    // =======================================================================
+    // === Generate messages in JSON object node format
+
+    private Highlights trafficSummary(LinkStatsType type) {
+        Highlights highlights = new Highlights();
+
+        // compile a set of bilinks (combining pairs of unidirectional links)
+        Map<LinkKey, BiLink> linkMap = new HashMap<>();
+        compileLinks(linkMap);
+        addEdgeLinks(linkMap);
+
+        for (BiLink blink : linkMap.values()) {
+            if (type == LinkStatsType.FLOW_STATS) {
+                attachFlowLoad(blink);
+            } else if (type == LinkStatsType.PORT_STATS) {
+                attachPortLoad(blink);
+            }
+
+            // we only want to report on links deemed to have traffic
+            if (blink.hasTraffic()) {
+                highlights.add(blink.generateHighlight(type));
+            }
+        }
+        return highlights;
+    }
+
+    // create highlights for links, showing flows for selected devices.
+    private Highlights deviceLinkFlows() {
+        Highlights highlights = new Highlights();
+
+        if (selectedNodes != null && !selectedNodes.devices().isEmpty()) {
+            // capture flow counts on bilinks
+            Map<LinkKey, BiLink> linkMap = new HashMap<>();
+
+            for (Device device : selectedNodes.devices()) {
+                Map<Link, Integer> counts = getLinkFlowCounts(device.id());
+                for (Link link : counts.keySet()) {
+                    BiLink blink = TopoUtils.addLink(linkMap, link);
+                    blink.addFlows(counts.get(link));
+                }
+            }
+
+            // now report on our collated links
+            for (BiLink blink : linkMap.values()) {
+                highlights.add(blink.generateHighlight(LinkStatsType.FLOW_COUNT));
+            }
+
+        }
+        return highlights;
+    }
+
+    private Highlights intentGroup() {
+        Highlights highlights = new Highlights();
+
+        if (selectedIntents != null && !selectedIntents.none()) {
+            // If 'all' intents are selected, they will all have primary
+            // highlighting; otherwise, the specifically selected intent will
+            // have primary highlighting, and the remainder will have secondary
+            // highlighting.
+            Set<Intent> primary;
+            Set<Intent> secondary;
+            int count = selectedIntents.size();
+
+            Set<Intent> allBut = new HashSet<>(selectedIntents.intents());
+            Intent current;
+
+            if (selectedIntents.all()) {
+                primary = allBut;
+                secondary = Collections.emptySet();
+                log.debug("Highlight all intents ({})", count);
+            } else {
+                current = selectedIntents.current();
+                primary = new HashSet<>();
+                primary.add(current);
+                allBut.remove(current);
+                secondary = allBut;
+                log.debug("Highlight intent: {} ([{}] of {})",
+                          current.id(), selectedIntents.index(), count);
+            }
+            TrafficClass tc1 = new TrafficClass(PRIMARY_HIGHLIGHT, primary);
+            TrafficClass tc2 = new TrafficClass(SECONDARY_HIGHLIGHT, secondary);
+            // classify primary links after secondary (last man wins)
+            highlightIntents(highlights, tc2, tc1);
+        }
+        return highlights;
+    }
+
+    private Highlights intentTraffic() {
+        Highlights highlights = new Highlights();
+
+        if (selectedIntents != null && selectedIntents.single()) {
+            Intent current = selectedIntents.current();
+            Set<Intent> primary = new HashSet<>();
+            primary.add(current);
+            log.debug("Highlight traffic for intent: {} ([{}] of {})",
+                      current.id(), selectedIntents.index(), selectedIntents.size());
+            TrafficClass tc1 = new TrafficClass(PRIMARY_HIGHLIGHT, primary, true);
+            highlightIntents(highlights, tc1);
+        }
+        return highlights;
+    }
+
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    private void compileLinks(Map<LinkKey, BiLink> linkMap) {
+        servicesBundle.linkService().getLinks()
+                .forEach(link -> TopoUtils.addLink(linkMap, link));
+    }
+
+    private void addEdgeLinks(Map<LinkKey, BiLink> biLinks) {
+        servicesBundle.hostService().getHosts().forEach(host -> {
+            TopoUtils.addLink(biLinks, createEdgeLink(host, true));
+            TopoUtils.addLink(biLinks, createEdgeLink(host, false));
+        });
+    }
+
+    private Load getLinkFlowLoad(Link link) {
+        if (link != null && link.src().elementId() instanceof DeviceId) {
+            return servicesBundle.flowStatsService().load(link);
+        }
+        return null;
+    }
+
+    private void attachFlowLoad(BiLink link) {
+        link.addLoad(getLinkFlowLoad(link.one()));
+        link.addLoad(getLinkFlowLoad(link.two()));
+    }
+
+    private void attachPortLoad(BiLink link) {
+        // For bi-directional traffic links, use
+        // the max link rate of either direction
+        // (we choose 'one' since we know that is never null)
+        Link one = link.one();
+        Load egressSrc = servicesBundle.portStatsService().load(one.src());
+        Load egressDst = servicesBundle.portStatsService().load(one.dst());
+//        link.addLoad(maxLoad(egressSrc, egressDst), BPS_THRESHOLD);
+        link.addLoad(maxLoad(egressSrc, egressDst), 10);    // FIXME - debug only
+    }
+
+    private Load maxLoad(Load a, Load b) {
+        if (a == null) {
+            return b;
+        }
+        if (b == null) {
+            return a;
+        }
+        return a.rate() > b.rate() ? a : b;
+    }
+
+    // ---
+
+    // Counts all flow entries that egress on the links of the given device.
+    private Map<Link, Integer> getLinkFlowCounts(DeviceId deviceId) {
+        // get the flows for the device
+        List<FlowEntry> entries = new ArrayList<>();
+        for (FlowEntry flowEntry : servicesBundle.flowService().getFlowEntries(deviceId)) {
+            entries.add(flowEntry);
+        }
+
+        // get egress links from device, and include edge links
+        Set<Link> links = new HashSet<>(servicesBundle.linkService().getDeviceEgressLinks(deviceId));
+        Set<Host> hosts = servicesBundle.hostService().getConnectedHosts(deviceId);
+        if (hosts != null) {
+            for (Host host : hosts) {
+                links.add(createEdgeLink(host, false));
+            }
+        }
+
+        // compile flow counts per link
+        Map<Link, Integer> counts = new HashMap<>();
+        for (Link link : links) {
+            counts.put(link, getEgressFlows(link, entries));
+        }
+        return counts;
+    }
+
+    // Counts all entries that egress on the link source port.
+    private int getEgressFlows(Link link, List<FlowEntry> entries) {
+        int count = 0;
+        PortNumber out = link.src().port();
+        for (FlowEntry entry : entries) {
+            TrafficTreatment treatment = entry.treatment();
+            for (Instruction instruction : treatment.allInstructions()) {
+                if (instruction.type() == Instruction.Type.OUTPUT &&
+                        ((OutputInstruction) instruction).port().equals(out)) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+    // ---
+    private void highlightIntents(Highlights highlights,
+                                  TrafficClass... trafficClasses) {
+        Map<LinkKey, BiLink> linkMap = new HashMap<>();
+
+
+        for (TrafficClass trafficClass : trafficClasses) {
+            classifyLinkTraffic(linkMap, trafficClass);
+        }
+
+        for (BiLink blink : linkMap.values()) {
+            highlights.add(blink.generateHighlight(LinkStatsType.TAGGED));
+        }
+    }
+
+    private void classifyLinkTraffic(Map<LinkKey, BiLink> linkMap,
+                                     TrafficClass trafficClass) {
+        for (Intent intent : trafficClass.intents()) {
+            boolean isOptical = intent instanceof OpticalConnectivityIntent;
+            List<Intent> installables = servicesBundle.intentService()
+                    .getInstallableIntents(intent.key());
+            Iterable<Link> links = null;
+
+            if (installables != null) {
+                for (Intent installable : installables) {
+
+                    if (installable instanceof PathIntent) {
+                        links = ((PathIntent) installable).path().links();
+                    } else if (installable instanceof FlowRuleIntent) {
+                        links = linkResources(installable);
+                    } else if (installable instanceof LinkCollectionIntent) {
+                        links = ((LinkCollectionIntent) installable).links();
+                    } else if (installable instanceof OpticalPathIntent) {
+                        links = ((OpticalPathIntent) installable).path().links();
+                    }
+
+                    classifyLinks(trafficClass, isOptical, linkMap, links);
+                }
+            }
+        }
+    }
+
+    private void classifyLinks(TrafficClass trafficClass, boolean isOptical,
+                               Map<LinkKey, BiLink> linkMap,
+                               Iterable<Link> links) {
+        if (links != null) {
+            for (Link link : links) {
+                BiLink blink = TopoUtils.addLink(linkMap, link);
+                if (trafficClass.showTraffic()) {
+                    blink.addLoad(getLinkFlowLoad(link));
+                    blink.setAntMarch(true);
+                }
+                blink.setOptical(isOptical);
+                blink.tagFlavor(trafficClass.flavor());
+            }
+        }
+    }
+
+    // Extracts links from the specified flow rule intent resources
+    private Collection<Link> linkResources(Intent installable) {
+        ImmutableList.Builder<Link> builder = ImmutableList.builder();
+        installable.resources().stream().filter(r -> r instanceof Link)
+                .forEach(r -> builder.add((Link) r));
+        return builder.build();
+    }
+
+    // =======================================================================
+    // === Background Task
+
+    // Provides periodic update of traffic information to the client
+    private class TrafficMonitor extends TimerTask {
+        @Override
+        public void run() {
+            try {
+                switch (mode) {
+                    case ALL_FLOW_TRAFFIC:
+                        sendAllFlowTraffic();
+                        break;
+                    case ALL_PORT_TRAFFIC:
+                        sendAllPortTraffic();
+                        break;
+                    case DEV_LINK_FLOWS:
+                        sendDeviceLinkFlows();
+                        break;
+                    case SEL_INTENT:
+                        sendSelectedIntentTraffic();
+                        break;
+
+                    default:
+                        // RELATED_INTENTS and IDLE modes should never invoke
+                        // the background task, but if they do, they have
+                        // nothing to do
+                        break;
+                }
+
+            } catch (Exception e) {
+                log.warn("Unable to process traffic task due to {}", e.getMessage());
+                log.warn("Boom!", e);
+            }
+        }
+    }
+
+}