ONOS-6259: Topo2 - Implement server-side highlighting model
- NOTE: Still WIP
- Implement doAggregation() in Traffic2Monitor.
- Plumb through call to get relevantSynthLinks().
- Create UiLinkId from LinkKey.
- Add reference to original UiLink in the UiSynthLink, (so we can use as a key later).
- TrafficLink enhancements:
-- Implement equals/hashCode
-- add a copy constructor
-- add mergeStats() method
-- add stats accessor methods

Change-Id: I693626971b3511b842e80cee7fcd2a252087597f
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiLinkId.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiLinkId.java
index c8dae05..dd1b3b8 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiLinkId.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiLinkId.java
@@ -16,11 +16,13 @@
 
 package org.onosproject.ui.model.topo;
 
+import org.onlab.util.Identifier;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.ElementId;
 import org.onosproject.net.HostId;
 import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.region.RegionId;
 
@@ -40,7 +42,7 @@
     private static final String E_IDENTICAL = "Region IDs cannot be same";
 
     private static final Comparator<RegionId> REGION_ID_COMPARATOR =
-            (o1, o2) -> o1.toString().compareTo(o2.toString());
+            Comparator.comparing(Identifier::toString);
 
     /**
      * Designates the directionality of an underlying (uni-directional) link.
@@ -257,16 +259,28 @@
      *
      * @param link link for which the identifier is required
      * @return link identifier
-     * @throws NullPointerException if any of the required fields are null
+     * @throws NullPointerException if src or dst connect point is null
      */
     public static UiLinkId uiLinkId(Link link) {
-        ConnectPoint src = link.src();
-        ConnectPoint dst = link.dst();
+        return canonicalizeIdentifier(link.src(), link.dst());
+    }
+
+    /**
+     * Creates the canonical link identifier from the given link key.
+     *
+     * @param lk link key
+     * @return equivalent link identifier
+     * @throws NullPointerException if src or dst connect point is null
+     */
+    public static UiLinkId uiLinkId(LinkKey lk) {
+        return canonicalizeIdentifier(lk.src(), lk.dst());
+    }
+
+    private static UiLinkId canonicalizeIdentifier(ConnectPoint src, ConnectPoint dst) {
         if (src == null || dst == null) {
             throw new NullPointerException(
-                    "null src or dst connect point: " + link);
+                    "null src or dst connect point (illegal for UiLinkId)");
         }
-
         ElementId srcId = src.elementId();
         ElementId dstId = dst.elementId();
 
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiSynthLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiSynthLink.java
index 7e67430..af1dfa7 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiSynthLink.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiSynthLink.java
@@ -28,16 +28,20 @@
 
     private final RegionId regionId;
     private final UiLink link;
+    private final UiLink original;
 
     /**
      * Constructs a synthetic link with the given parameters.
      *
      * @param regionId the region to which the link belongs
      * @param link     the link instance
+     * @param original the original link (device or edge)
+     *                 from which this was derived
      */
-    public UiSynthLink(RegionId regionId, UiLink link) {
+    public UiSynthLink(RegionId regionId, UiLink link, UiLink original) {
         this.regionId = regionId;
         this.link = link;
+        this.original = original;
     }
 
     @Override
@@ -45,6 +49,7 @@
         return toStringHelper(this)
                 .add("region", regionId)
                 .add("link", link)
+                .add("original", original)
                 .toString();
     }
 
@@ -65,4 +70,13 @@
     public UiLink link() {
         return link;
     }
+
+    /**
+     * Returns the original link from which this was derived.
+     *
+     * @return the original link
+     */
+    public UiLink original() {
+        return original;
+    }
 }
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopology.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopology.java
index 8c8740b..ff21176 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopology.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopology.java
@@ -56,7 +56,7 @@
     private static final Logger log = LoggerFactory.getLogger(UiTopology.class);
 
     private static final Comparator<UiClusterMember> CLUSTER_MEMBER_COMPARATOR =
-            (o1, o2) -> o1.idAsString().compareTo(o2.idAsString());
+            Comparator.comparing(UiClusterMember::idAsString);
 
 
     // top level mappings of topology elements by ID
@@ -67,8 +67,8 @@
     private final Map<UiLinkId, UiDeviceLink> devLinkLookup = new HashMap<>();
     private final Map<UiLinkId, UiEdgeLink> edgeLinkLookup = new HashMap<>();
 
-    // a cache of the computed synthetic links
-    private final List<UiSynthLink> synthLinks = new ArrayList<>();
+    // a cache of the computed synthetic links, keyed by ID of original UiLink
+    private final Map<UiLinkId, UiSynthLink> synthMap = new HashMap<>();
 
     // a container for devices, hosts, etc. belonging to no region
     private final UiRegion nullRegion = new UiRegion(this, null);
@@ -93,7 +93,7 @@
                 .add("#hosts", hostLookup.size())
                 .add("#dev-links", devLinkLookup.size())
                 .add("#edge-links", edgeLinkLookup.size())
-                .add("#synth-links", synthLinks.size())
+                .add("#synth-links", synthMap.size())
                 .toString();
     }
 
@@ -115,7 +115,7 @@
         devLinkLookup.clear();
         edgeLinkLookup.clear();
 
-        synthLinks.clear();
+        synthMap.clear();
 
         nullRegion.destroy();
     }
@@ -507,8 +507,10 @@
             slinks.addAll(wrapHostLinks(r));
         }
 
-        synthLinks.clear();
-        synthLinks.addAll(slinks);
+        synthMap.clear();
+        for (UiSynthLink sl : slinks) {
+            synthMap.put(sl.original().id(), sl);
+        }
     }
 
     private Set<UiSynthLink> wrapHostLinks(UiRegion region) {
@@ -519,7 +521,7 @@
 
     private UiSynthLink wrapHostLink(RegionId regionId, UiHost host) {
         UiEdgeLink elink = new UiEdgeLink(this, host.edgeLinkId());
-        return new UiSynthLink(regionId, elink);
+        return new UiSynthLink(regionId, elink, elink);
     }
 
     private UiSynthLink inferSyntheticLink(UiDeviceLink link) {
@@ -637,7 +639,7 @@
                 link = orig;
             }
         }
-        return new UiSynthLink(commonRegion, link);
+        return new UiSynthLink(commonRegion, link, orig);
     }
 
     private List<RegionId> ancestors(DeviceId id) {
@@ -667,7 +669,7 @@
      * @return synthetic links for this region
      */
     public List<UiSynthLink> findSynthLinks(RegionId regionId) {
-        return synthLinks.stream()
+        return synthMap.values().stream()
                 .filter(s -> Objects.equals(regionId, s.regionId()))
                 .collect(Collectors.toList());
     }
@@ -679,7 +681,7 @@
      * @return the synthetic link count
      */
     public int synthLinkCount() {
-        return synthLinks.size();
+        return synthMap.size();
     }
 
     /**
@@ -722,7 +724,7 @@
         }
 
         sb.append(INDENT_1).append("Synth Links").append(EOL);
-        for (UiSynthLink link : synthLinks) {
+        for (UiSynthLink link : synthMap.values()) {
             sb.append(INDENT_2).append(link).append(EOL);
         }
         sb.append("------").append(EOL);
diff --git a/core/api/src/test/java/org/onosproject/ui/model/topo/UiLinkIdTest.java b/core/api/src/test/java/org/onosproject/ui/model/topo/UiLinkIdTest.java
index 80666d6..39d4cb4 100644
--- a/core/api/src/test/java/org/onosproject/ui/model/topo/UiLinkIdTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/model/topo/UiLinkIdTest.java
@@ -23,6 +23,7 @@
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.HostId;
 import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.provider.ProviderId;
 import org.onosproject.net.region.RegionId;
@@ -164,4 +165,20 @@
         assertEquals("port", P1, id.portB());
     }
 
+    @Test
+    public void fromLinkKey() {
+        title("fromLinkKey");
+
+        LinkKey lk1 = LinkKey.linkKey(CP_X1, CP_Y2);
+        print("link-key-1: %s", lk1);
+        LinkKey lk2 = LinkKey.linkKey(CP_Y2, CP_X1);
+        print("link-key-2: %s", lk2);
+
+        UiLinkId id1 = UiLinkId.uiLinkId(lk1);
+        print("identifier-1: %s", id1);
+        UiLinkId id2 = UiLinkId.uiLinkId(lk2);
+        print("identifier-2: %s", id2);
+
+        assertEquals("unequal canon-ids", id1, id2);
+    }
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2TrafficMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2TrafficMessageHandler.java
index ce7175b..687764d 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2TrafficMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2TrafficMessageHandler.java
@@ -24,12 +24,16 @@
 import org.onosproject.ui.UiConnection;
 import org.onosproject.ui.UiMessageHandler;
 import org.onosproject.ui.impl.TrafficMonitorBase.Mode;
+import org.onosproject.ui.impl.UiWebSocket;
 import org.onosproject.ui.impl.topo.util.ServicesBundle;
+import org.onosproject.ui.model.topo.UiLinkId;
+import org.onosproject.ui.model.topo.UiSynthLink;
 import org.onosproject.ui.topo.Highlights;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Collection;
+import java.util.Map;
 
 import static org.onosproject.ui.topo.TopoJson.topo2HighlightsMessage;
 
@@ -44,9 +48,6 @@
     private static final String REQUEST_ALL_TRAFFIC = "topo2RequestAllTraffic";
     private static final String CANCEL_TRAFFIC = "topo2CancelTraffic";
 
-    // === Outbound event identifiers
-    private static final String HIGHLIGHTS = "topo2Highlights";
-
     // field values
     private static final String TRAFFIC_TYPE = "trafficType";
     private static final String FLOW_STATS_BYTES = "flowStatsBytes";
@@ -56,13 +57,9 @@
     // configuration parameters
     private static final long TRAFFIC_PERIOD = 5000;
 
-//    private UiTopoSession topoSession;
-//    private Topo2Jsonifier t2json;
-
     protected ServicesBundle services;
-    private String version;
 
-
+    private UiTopoSession topoSession;
     private Traffic2Monitor traffic;
 
 
@@ -71,13 +68,8 @@
         super.init(connection, directory);
 
         services = new ServicesBundle(directory);
-
         traffic = new Traffic2Monitor(TRAFFIC_PERIOD, services, this);
-
-        // get the topo session from the UiWebSocket
-//        topoSession = ((UiWebSocket) connection).topoSession();
-//        t2json = new Topo2Jsonifier(directory, connection.userName());
-
+        topoSession = ((UiWebSocket) connection).topoSession();
     }
 
     @Override
@@ -104,6 +96,16 @@
         sendMessage(topo2HighlightsMessage(highlights));
     }
 
+    /**
+     * Asks the topo session for the relevant synth links for current region.
+     * The returned map is keyed by "original" link.
+     *
+     * @return synth link map
+     */
+    Map<UiLinkId, UiSynthLink> retrieveRelevantSynthLinks() {
+        return topoSession.relevantSynthLinks();
+    }
+
     // ==================================================================
 
     private final class Topo2AllTraffic extends RequestHandler {
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 8f056fa..60e7e83 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
@@ -20,10 +20,15 @@
 import org.onosproject.ui.impl.TrafficMonitorBase;
 import org.onosproject.ui.impl.topo.util.ServicesBundle;
 import org.onosproject.ui.impl.topo.util.TrafficLink;
+import org.onosproject.ui.model.topo.UiLinkId;
+import org.onosproject.ui.model.topo.UiSynthLink;
 import org.onosproject.ui.topo.Highlights;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -95,9 +100,34 @@
 
     @Override
     protected Set<TrafficLink> doAggregation(Set<TrafficLink> linksWithTraffic) {
-        // TODO: figure out how to aggregate the link data...
         log.debug("Need to aggregate {} links", linksWithTraffic.size());
 
-        return linksWithTraffic;
+        // first, retrieve from the shared topology model those synth links that
+        // are part of the region currently being viewed by the user...
+        Map<UiLinkId, UiSynthLink> synthLinkMap =
+                msgHandler.retrieveRelevantSynthLinks();
+
+        // NOTE: compute Set<TrafficLink> which represents the consolidated links
+
+        Map<UiLinkId, TrafficLink> mappedByUiLinkId = new HashMap<>();
+
+        for (TrafficLink tl : linksWithTraffic) {
+            UiLinkId tlid = UiLinkId.uiLinkId(tl.key());
+            UiSynthLink sl = synthLinkMap.get(tlid);
+            if (sl != null) {
+                UiLinkId aggrid = sl.link().id();
+                TrafficLink aggregated = mappedByUiLinkId.get(aggrid);
+                if (aggregated == null) {
+                    aggregated = new TrafficLink(tl);
+                    mappedByUiLinkId.put(aggrid, aggregated);
+                } else {
+                    aggregated.mergeStats(tl);
+                }
+            }
+        }
+
+        Set<TrafficLink> result = new HashSet<>();
+        result.addAll(mappedByUiLinkId.values());
+        return result;
     }
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoSession.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoSession.java
index 3a3f141..2a02ffb 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoSession.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoSession.java
@@ -19,6 +19,7 @@
 import org.onosproject.net.region.RegionId;
 import org.onosproject.ui.UiTopoLayoutService;
 import org.onosproject.ui.impl.UiWebSocket;
+import org.onosproject.ui.model.topo.UiLinkId;
 import org.onosproject.ui.model.topo.UiModelEvent;
 import org.onosproject.ui.impl.topo.model.UiModelListener;
 import org.onosproject.ui.impl.topo.model.UiSharedTopologyModel;
@@ -33,6 +34,7 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -280,4 +282,14 @@
         UiTopoLayout layout = layoutService.getLayout(r);
         setCurrentLayout(layout);
     }
+
+    /**
+     * Returns synthetic links that are in the current region, mapped by
+     * original link ID.
+     *
+     * @return map of synth links
+     */
+    public Map<UiLinkId, UiSynthLink> relevantSynthLinks() {
+        return sharedModel.relevantSynthLinks(currentLayout.regionId());
+    }
 }
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 ec07673..fa426e1 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
@@ -49,8 +49,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
@@ -553,6 +555,14 @@
         return uiTopology.findSynthLinks(regionId);
     }
 
+    Map<UiLinkId, UiSynthLink> relevantSynthLinks(RegionId regionId) {
+        Map<UiLinkId, UiSynthLink> result = new HashMap<>();
+        for (UiSynthLink sl : getSynthLinks(regionId)) {
+            result.put(sl.original().id(), sl);
+        }
+        return result;
+    }
+
     /**
      * Refreshes the internal state.
      */
@@ -569,11 +579,6 @@
 
         services.region().getRegions().forEach(r -> {
             RegionId rid = r.id();
-
-//            BasicRegionConfig rcfg = cfgService.getConfig(rid, BasicRegionConfig.class);
-//            services.netcfg() ...
-            // TODO: figure out how to include peer-location data in UiRegion instance
-
             UiRegion region = uiTopology.findRegion(rid);
             if (region != null) {
                 reconcileDevicesAndHostsWithRegion(allDevices, allHosts, rid, region);
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 6500ae3..183ccd6 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
@@ -67,6 +67,7 @@
 import org.onosproject.ui.model.topo.UiDevice;
 import org.onosproject.ui.model.topo.UiDeviceLink;
 import org.onosproject.ui.model.topo.UiHost;
+import org.onosproject.ui.model.topo.UiLinkId;
 import org.onosproject.ui.model.topo.UiModelEvent;
 import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiSynthLink;
@@ -74,6 +75,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 
@@ -302,6 +304,17 @@
         return cache.getSynthLinks(regionId);
     }
 
+    /**
+     * Returns the synthetic links associated with the specified region,
+     * mapped by original link id.
+     *
+     * @param regionId region ID
+     * @return map of synthetic links for that region
+     */
+    public Map<UiLinkId, UiSynthLink> relevantSynthLinks(RegionId regionId) {
+        return cache.relevantSynthLinks(regionId);
+    }
+
     // =====================================================================
 
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/TrafficLink.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/TrafficLink.java
index 166a83a..f2643c6 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/TrafficLink.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/TrafficLink.java
@@ -29,6 +29,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
+import static com.google.common.base.MoreObjects.toStringHelper;
 import static org.onosproject.ui.topo.LinkHighlight.Flavor.NO_HIGHLIGHT;
 import static org.onosproject.ui.topo.LinkHighlight.Flavor.PRIMARY_HIGHLIGHT;
 import static org.onosproject.ui.topo.LinkHighlight.Flavor.SECONDARY_HIGHLIGHT;
@@ -70,6 +71,91 @@
     }
 
     /**
+     * Copy constructor.
+     *
+     * @param copy the instance to copy
+     */
+    public TrafficLink(TrafficLink copy) {
+        super(copy.key(), copy.one());
+        setOther(copy.two());
+        bytes = copy.bytes;
+        rate = copy.rate;
+        flows = copy.flows;
+        taggedFlavor = copy.taggedFlavor;
+        hasTraffic = copy.hasTraffic;
+        isOptical = copy.isOptical;
+        antMarch = copy.antMarch;
+        mods.addAll(copy.mods);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        TrafficLink that = (TrafficLink) o;
+
+        return bytes == that.bytes && rate == that.rate &&
+                flows == that.flows && hasTraffic == that.hasTraffic &&
+                isOptical == that.isOptical && antMarch == that.antMarch &&
+                taggedFlavor == that.taggedFlavor && mods.equals(that.mods);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (int) (bytes ^ (bytes >>> 32));
+        result = 31 * result + (int) (rate ^ (rate >>> 32));
+        result = 31 * result + (int) (flows ^ (flows >>> 32));
+        result = 31 * result + taggedFlavor.hashCode();
+        result = 31 * result + (hasTraffic ? 1 : 0);
+        result = 31 * result + (isOptical ? 1 : 0);
+        result = 31 * result + (antMarch ? 1 : 0);
+        result = 31 * result + mods.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("key", key())
+                .add("bytes", bytes)
+                .add("rate", rate)
+                .add("flows", flows)
+                .toString();
+    }
+
+    /**
+     * Returns the count of bytes.
+     *
+     * @return the byte count
+     */
+    public long bytes() {
+        return bytes;
+    }
+
+    /**
+     * Returns the rate.
+     *
+     * @return the rate
+     */
+    public long rate() {
+        return rate;
+    }
+
+    /**
+     * Returns the flows.
+     *
+     * @return flow count
+     */
+    public long flows() {
+        return flows;
+    }
+
+    /**
      * Sets the optical flag to the given value.
      *
      * @param b true if an optical link
@@ -149,6 +235,18 @@
         this.flows += count;
     }
 
+    /**
+     * Merges the load recorded on the given traffic link into this one.
+     *
+     * @param other the other traffic link
+     */
+    public void mergeStats(TrafficLink other) {
+        this.bytes += other.bytes;
+        this.rate += other.rate;
+        this.flows += other.flows;
+    }
+
+
     @Override
     public LinkHighlight highlight(Enum<?> type) {
         StatsType statsType = (StatsType) type;
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/util/TrafficLinkTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/util/TrafficLinkTest.java
new file mode 100644
index 0000000..2afa8a0
--- /dev/null
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/util/TrafficLinkTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017-present 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.topo.util;
+
+import org.junit.Test;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultEdgeLink;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.statistic.DefaultLoad;
+import org.onosproject.net.statistic.Load;
+import org.onosproject.ui.impl.AbstractUiImplTest;
+import org.onosproject.ui.topo.TopoUtils;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+
+/**
+ * Unit tests for {@link TrafficLink}.
+ */
+public class TrafficLinkTest extends AbstractUiImplTest {
+
+    private static final DeviceId D1 = deviceId("1");
+    private static final DeviceId D2 = deviceId("2");
+    private static final PortNumber P1 = portNumber(1);
+    private static final PortNumber P2 = portNumber(2);
+
+    private static final ConnectPoint SRC1 = new ConnectPoint(D1, P1);
+    private static final ConnectPoint DST1 = new ConnectPoint(D2, P1);
+    private static final ConnectPoint DST2 = new ConnectPoint(D2, P2);
+
+    private static final LinkKey X = LinkKey.linkKey(SRC1, DST2);
+
+
+    private TrafficLink createALink() {
+        Link linkIngress = DefaultEdgeLink.createEdgeLink(SRC1, true);
+        LinkKey key = TopoUtils.canonicalLinkKey(checkNotNull(linkIngress));
+        TrafficLink tl = new TrafficLink(key, linkIngress);
+        Link linkEgress = DefaultEdgeLink.createEdgeLink(SRC1, false);
+        tl.setOther(linkEgress);
+        return tl;
+    }
+
+    @Test
+    public void basic() {
+        title("basic");
+
+        TrafficLink tl = createALink();
+        Load bigLoad = new DefaultLoad(2000, 0);
+        tl.addLoad(bigLoad);
+        print(tl);
+        assertEquals("bad bytes value", 2000, tl.bytes());
+        // NOTE: rate is bytes / period (10 seconds)
+        assertEquals("bad rate value", 200, tl.rate());
+        // this load does not represent flows
+        assertEquals("bad flow count", 0, tl.flows());
+    }
+
+    @Test
+    public void copyConstructor() {
+        title("copy-constructor");
+        TrafficLink tlOrig = createALink();
+        TrafficLink tlCopy = new TrafficLink(tlOrig);
+        assertEquals("not copied correctly (1)", tlOrig, tlCopy);
+
+        tlOrig.addLoad(new DefaultLoad(2000, 0));
+        tlCopy = new TrafficLink(tlOrig);
+        assertEquals("not copied correctly (2)", tlOrig, tlCopy);
+
+        tlOrig = createALink();
+        tlOrig.addFlows(345);
+        tlCopy = new TrafficLink(tlOrig);
+        assertEquals("not copied correctly (3)", tlOrig, tlCopy);
+    }
+
+    @Test
+    public void mergeStatsBytes() {
+        title("mergeStatsBytes");
+        TrafficLink tla = createALink();
+        tla.addLoad(new DefaultLoad(2000, 0));
+        print(tla);
+
+        TrafficLink tlb = createALink();
+        tlb.addLoad(new DefaultLoad(3000, 0));
+        print(tlb);
+
+        tla.mergeStats(tlb);
+        print(tla);
+        assertEquals("mergedBytes", 5000, tla.bytes());
+        assertEquals("mergeRate", 500, tla.rate());
+    }
+
+    @Test
+    public void mergeStatsFlows() {
+        title("mergeStatsFlows");
+        TrafficLink tla = createALink();
+        tla.addFlows(9);
+        print(tla);
+
+        TrafficLink tlb = createALink();
+        tlb.addFlows(16);
+        print(tlb);
+
+        tla.mergeStats(tlb);
+        print(tla);
+        assertEquals("mergedFlows", 25, tla.flows());
+    }
+}