Topo2 - UiSynthLink aggregation in JSON structure.
- augment UiLinkId with Type field
- preparing for new JSON structure in Topo2Jsonifier
- additional unit tests

Change-Id: Ib8628fc26f72c0369bf8358af905bc6a75e31397
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 dd1b3b8..a644ba3 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
@@ -38,11 +38,15 @@
 
     private static final String E_PORT_NULL = "Port number cannot be null";
     private static final String E_DEVICE_ID_NULL = "Device ID cannot be null";
+    private static final String E_HOST_ID_NULL = "Host ID cannot be null";
     private static final String E_REGION_ID_NULL = "Region ID cannot be null";
     private static final String E_IDENTICAL = "Region IDs cannot be same";
 
     private static final Comparator<RegionId> REGION_ID_COMPARATOR =
             Comparator.comparing(Identifier::toString);
+    private static final Comparator<DeviceId> DEVICE_ID_COMPARATOR =
+            Comparator.comparing(DeviceId::toString);
+
 
     /**
      * Designates the directionality of an underlying (uni-directional) link.
@@ -52,6 +56,16 @@
         B_TO_A
     }
 
+    /**
+     * Designates the type of link the identifier represents.
+     */
+    public enum Type {
+        REGION_REGION,
+        REGION_DEVICE,
+        DEVICE_DEVICE,
+        HOST_DEVICE
+    }
+
     static final String CP_DELIMITER = "~";
     static final String ID_PORT_DELIMITER = "/";
 
@@ -67,6 +81,8 @@
     private final String idA;
     private final String idB;
 
+    private final Type type;
+
     /**
      * Creates a UI link identifier. It is expected that A comes before B when
      * the two identifiers are naturally sorted, thus providing a representation
@@ -87,10 +103,14 @@
         regionA = null;
         regionB = null;
 
+        boolean isEdgeLink = (a instanceof HostId);
+
         // NOTE: for edgelinks, hosts are always element A
-        idA = (a instanceof HostId) ? a.toString() : a + ID_PORT_DELIMITER + pa;
+        idA = isEdgeLink ? a.toString() : a + ID_PORT_DELIMITER + pa;
         idB = b + ID_PORT_DELIMITER + pb;
         idStr = idA + CP_DELIMITER + idB;
+
+        type = isEdgeLink ? Type.HOST_DEVICE : Type.DEVICE_DEVICE;
     }
 
     /**
@@ -112,6 +132,8 @@
         idA = a.toString();
         idB = b.toString();
         idStr = idA + CP_DELIMITER + idB;
+
+        type = Type.REGION_REGION;
     }
 
     /**
@@ -134,6 +156,8 @@
         idA = r.toString();
         idB = elementB + ID_PORT_DELIMITER + portB;
         idStr = idA + CP_DELIMITER + idB;
+
+        type = Type.REGION_DEVICE;
     }
 
     @Override
@@ -221,6 +245,52 @@
         return regionB;
     }
 
+    /**
+     * Returns the type of link this identifier represents.
+     *
+     * @return the link identifier type
+     */
+    public Type type() {
+        return type;
+    }
+
+    /**
+     * Returns true if this identifier represents a region-region link.
+     *
+     * @return true if region-region link identifier; false otherwise
+     */
+    public boolean isRegionRegion() {
+        return type == Type.REGION_REGION;
+    }
+
+    /**
+     * Returns true if this identifier represents a region-device link.
+     *
+     * @return true if region-device link identifier; false otherwise
+     */
+    public boolean isRegionDevice() {
+        return type == Type.REGION_DEVICE;
+    }
+
+    /**
+     * Returns true if this identifier represents a device-device
+     * (infrastructure) link.
+     *
+     * @return true if device-device link identifier; false otherwise
+     */
+    public boolean isDeviceDevice() {
+        return type == Type.DEVICE_DEVICE;
+    }
+
+    /**
+     * Returns true if this identifier represents a host-device (edge) link.
+     *
+     * @return true if host-device link identifier; false otherwise
+     */
+    public boolean isHostDevice() {
+        return type == Type.HOST_DEVICE;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -327,4 +397,43 @@
 
         return new UiLinkId(regionId, deviceId, portNumber);
     }
+
+    /**
+     * Generates an identifier for a link between two devices.
+     *
+     * @param a  device A
+     * @param pa port A
+     * @param b  device B
+     * @param pb port B
+     * @return link identifier
+     * @throws NullPointerException if any of the required fields are null
+     */
+    public static UiLinkId uiLinkId(DeviceId a, PortNumber pa,
+                                    DeviceId b, PortNumber pb) {
+        checkNotNull(a, E_DEVICE_ID_NULL + " (A)");
+        checkNotNull(b, E_DEVICE_ID_NULL + " (B)");
+        checkNotNull(pa, E_PORT_NULL + " (A)");
+        checkNotNull(pb, E_PORT_NULL + " (B)");
+
+        boolean flip = DEVICE_ID_COMPARATOR.compare(a, b) > 0;
+        return flip ? new UiLinkId(b, pb, a, pa) : new UiLinkId(a, pa, b, pb);
+    }
+
+    /**
+     * Generates an identifier for an edge link. Note that host is always
+     * element A.
+     *
+     * @param h host
+     * @param d device
+     * @param p port
+     * @return link identifier
+     * @throws NullPointerException if any of the required fields are null
+     */
+    public static UiLinkId uiLinkId(HostId h, DeviceId d, PortNumber p) {
+        checkNotNull(h, E_HOST_ID_NULL);
+        checkNotNull(d, E_DEVICE_ID_NULL);
+        checkNotNull(p, E_PORT_NULL);
+        return new UiLinkId(h, PortNumber.P0, d, p);
+    }
+
 }
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 39d4cb4..65e0750 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
@@ -30,8 +30,10 @@
 import org.onosproject.ui.model.AbstractUiModelTest;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.onosproject.net.DeviceId.deviceId;
 import static org.onosproject.net.HostId.hostId;
 import static org.onosproject.net.PortNumber.P0;
@@ -120,6 +122,9 @@
         assertEquals("wrong element B", DEV_X, id.elementB());
         assertNull("region A?", id.regionA());
         assertNull("region B?", id.regionB());
+        assertEquals("not H-D", UiLinkId.Type.HOST_DEVICE, id.type());
+        assertTrue("not host-dev", id.isHostDevice());
+        assertFalse("dev-dev?", id.isDeviceDevice());
     }
 
     @Test
@@ -133,6 +138,8 @@
         assertEquals("wrong element B", DEV_Y, id.elementB());
         assertNull("region A?", id.regionA());
         assertNull("region B?", id.regionB());
+        assertEquals("not D-D", UiLinkId.Type.DEVICE_DEVICE, id.type());
+        assertTrue("not dev-dev", id.isDeviceDevice());
     }
 
     @Test
@@ -143,6 +150,8 @@
         print(" first: %s", idFirst);
         print("second: %s", idSecond);
         assertEquals("Not same ID", idFirst, idSecond);
+        assertEquals("not R-R", UiLinkId.Type.REGION_REGION, idFirst.type());
+        assertTrue("not reg-reg", idFirst.isRegionRegion());
     }
 
     @Test(expected = IllegalArgumentException.class)
@@ -163,6 +172,8 @@
         assertEquals("region ID", REG_1, id.regionA());
         assertEquals("device ID", DEV_X, id.elementB());
         assertEquals("port", P1, id.portB());
+        assertEquals("not R-D", UiLinkId.Type.REGION_DEVICE, id.type());
+        assertTrue("not reg-dev", id.isRegionDevice());
     }
 
     @Test
@@ -181,4 +192,39 @@
 
         assertEquals("unequal canon-ids", id1, id2);
     }
+
+    @Test
+    public void devToDevId() {
+        title("devToDevId");
+        UiLinkId id = UiLinkId.uiLinkId(DEV_X, P1, DEV_Y, P2);
+        print(id);
+        assertEquals("not dev x", DEV_X, id.elementA());
+        assertEquals("not dev y", DEV_Y, id.elementB());
+        assertEquals("not port 1", P1, id.portA());
+        assertEquals("not port 2", P2, id.portB());
+        assertTrue("not dev-dev", id.isDeviceDevice());
+    }
+
+    @Test
+    public void devToDevCanon() {
+        title("devToDevCanon");
+        UiLinkId id1 = UiLinkId.uiLinkId(DEV_X, P1, DEV_Y, P2);
+        UiLinkId id2 = UiLinkId.uiLinkId(DEV_Y, P2, DEV_X, P1);
+        print(id1);
+        print(id2);
+        assertEquals("not canonical", id1, id2);
+        assertEquals("not flipped", DEV_X, id2.elementA());
+    }
+
+    @Test
+    public void hostToDevId() {
+        title("hostToDevId");
+        UiLinkId id = UiLinkId.uiLinkId(HOST_A, DEV_Y, P2);
+        print(id);
+        assertEquals("not host a", HOST_A, id.elementA());
+        assertEquals("not port 0", P0, id.portA());
+        assertEquals("not dev y", DEV_Y, id.elementB());
+        assertEquals("not port 2", P2, id.portB());
+        assertTrue("not host-dev", id.isHostDevice());
+    }
 }
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 da1f8e4..8b5741a 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
@@ -46,12 +46,12 @@
 import org.onosproject.ui.UiPreferencesService;
 import org.onosproject.ui.UiTopoMap;
 import org.onosproject.ui.UiTopoMapFactory;
-import org.onosproject.ui.model.topo.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.UiModelEvent;
 import org.onosproject.ui.model.topo.UiNode;
 import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiSynthLink;
@@ -363,9 +363,13 @@
         return kids;
     }
 
-    private JsonNode jsonLinks(List<UiSynthLink> links) {
+    protected JsonNode jsonLinks(List<UiSynthLink> links) {
         ArrayNode synthLinks = arrayNode();
         links.forEach(l -> synthLinks.add(json(l)));
+
+        // TODO: implement the following........
+//        collateSynthLinks(synthLinks, links);
+
         return synthLinks;
     }
 
@@ -505,7 +509,7 @@
 
         } catch (NumberFormatException e) {
             log.warn("Invalid {} data: lat/Y={}, long/X={}",
-                    locType, values.get(0), values.get(1));
+                     locType, values.get(0), values.get(1));
         }
     }
 
@@ -517,9 +521,9 @@
             ObjectNode o = objectNode();
             for (LayoutLocation ll : locs) {
                 ObjectNode lnode = objectNode()
-                    .put(LOC_TYPE, ll.locType().toString())
-                    .put(LAT_OR_Y, ll.latOrY())
-                    .put(LONG_OR_X, ll.longOrX());
+                        .put(LOC_TYPE, ll.locType().toString())
+                        .put(LAT_OR_Y, ll.latOrY())
+                        .put(LONG_OR_X, ll.longOrX());
                 o.set(ll.id(), lnode);
             }
 
@@ -577,6 +581,13 @@
         return node;
     }
 
+    // TODO: add method to JSONify an aggregated collection of synth links
+
+    private void collateSynthLinks(ArrayNode array,
+                                   List<UiSynthLink> links) {
+        // TODO - combine via link id
+    }
+
     private ObjectNode json(UiSynthLink sLink) {
         return json(sLink.link());
     }
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java
index ab0cd2d..c1ff3de 100644
--- a/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java
@@ -16,19 +16,32 @@
 
 package org.onosproject.ui.impl.topo;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import org.junit.Test;
 import org.onosproject.net.Annotated;
 import org.onosproject.net.Annotations;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.region.RegionId;
 import org.onosproject.ui.impl.AbstractUiImplTest;
+import org.onosproject.ui.model.topo.UiDeviceLink;
+import org.onosproject.ui.model.topo.UiLink;
+import org.onosproject.ui.model.topo.UiLinkId;
 import org.onosproject.ui.model.topo.UiNode;
+import org.onosproject.ui.model.topo.UiRegionLink;
+import org.onosproject.ui.model.topo.UiSynthLink;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.region.RegionId.regionId;
+import static org.onosproject.ui.model.topo.UiLinkId.uiLinkId;
 import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
 import static org.onosproject.ui.model.topo.UiNode.LAYER_OPTICAL;
 import static org.onosproject.ui.model.topo.UiNode.LAYER_PACKET;
@@ -85,7 +98,7 @@
 
     @Test
     public void threeLayers() {
-        print("threeLayers()");
+        title("threeLayers()");
 
         List<Set<UiNode>> result = t2.splitByLayer(ALL_TAGS, NODES);
         print(result);
@@ -110,7 +123,7 @@
 
     @Test
     public void twoLayers() {
-        print("twoLayers()");
+        title("twoLayers()");
 
         List<Set<UiNode>> result = t2.splitByLayer(PKT_DEF_TAGS, NODES);
         print(result);
@@ -132,7 +145,7 @@
 
     @Test
     public void oneLayer() {
-        print("oneLayer()");
+        title("oneLayer()");
 
         List<Set<UiNode>> result = t2.splitByLayer(DEF_TAG_ONLY, NODES);
         print(result);
@@ -197,10 +210,86 @@
 
     @Test
     public void annotValues() {
-        print("annotValues()");
+        title("annotValues()");
         verifyValues(t2.getAnnotValues(THING, K1), V1);
         verifyValues(t2.getAnnotValues(THING, K3, K1), V3, V1);
         verifyValues(t2.getAnnotValues(THING, K1, K2, K3), V1, V2, V3);
         verifyValues(t2.getAnnotValues(THING, K1, K4));
     }
+
+
+    /*
+     * Test collation of region links in the following scenario...
+     *
+     *   Region A     Region B     Region C
+     *   +.......+    +.......+    +.......+
+     *   :  [1] -------- [3] -------- [5]  :
+     *   :   |   :    :   |   :    :   |   :
+     *   :   |   :    :   |   :    :   |   :
+     *   :  [2] -------- [4] -------- [6]  :
+     *   +.......+    +.......+    +.......+
+     */
+
+    private static PortNumber pn(long i) {
+        return PortNumber.portNumber(i);
+    }
+
+    private static UiLink rrLink(UiLinkId id) {
+        if (!id.isRegionRegion()) {
+            throw new IllegalArgumentException();
+        }
+        return new UiRegionLink(null, id);
+    }
+
+    private static UiLink ddLink(UiLinkId id) {
+        if (!id.isDeviceDevice()) {
+            throw new IllegalArgumentException();
+        }
+        return new UiDeviceLink(null, id);
+    }
+
+    private static final RegionId REGION_ROOT = regionId("root");
+    private static final RegionId REGION_A = regionId("rA");
+    private static final RegionId REGION_B = regionId("rB");
+    private static final RegionId REGION_C = regionId("rC");
+
+    private static final DeviceId DEV_1 = deviceId("d1");
+    private static final DeviceId DEV_2 = deviceId("d2");
+    private static final DeviceId DEV_3 = deviceId("d3");
+    private static final DeviceId DEV_4 = deviceId("d4");
+    private static final DeviceId DEV_5 = deviceId("d5");
+    private static final DeviceId DEV_6 = deviceId("d6");
+
+    private static final UiLinkId D1_D2 = uiLinkId(DEV_1, pn(2), DEV_2, pn(1));
+    private static final UiLinkId D1_D3 = uiLinkId(DEV_1, pn(3), DEV_3, pn(1));
+    private static final UiLinkId D2_D4 = uiLinkId(DEV_2, pn(4), DEV_4, pn(2));
+    private static final UiLinkId D3_D4 = uiLinkId(DEV_3, pn(4), DEV_4, pn(3));
+    private static final UiLinkId D3_D5 = uiLinkId(DEV_3, pn(5), DEV_5, pn(3));
+    private static final UiLinkId D4_D6 = uiLinkId(DEV_4, pn(6), DEV_6, pn(4));
+    private static final UiLinkId D5_D6 = uiLinkId(DEV_5, pn(6), DEV_6, pn(5));
+
+    private static final UiLinkId RA_RB = uiLinkId(REGION_A, REGION_B);
+    private static final UiLinkId RB_RC = uiLinkId(REGION_B, REGION_C);
+
+    private UiSynthLink makeSynth(RegionId container, UiLinkId rr, UiLinkId dd) {
+        return new UiSynthLink(container, rrLink(rr), ddLink(dd));
+    }
+
+    private List<UiSynthLink> createSynthLinks() {
+        List<UiSynthLink> links = new ArrayList<>();
+        links.add(makeSynth(REGION_ROOT, RA_RB, D1_D3));
+        links.add(makeSynth(REGION_ROOT, RA_RB, D2_D4));
+        links.add(makeSynth(REGION_ROOT, RB_RC, D3_D5));
+        links.add(makeSynth(REGION_ROOT, RB_RC, D4_D6));
+        return links;
+    }
+
+    @Test
+    public void encodeSynthLinks() {
+        title("encodeSynthLinks()");
+        JsonNode node = t2.jsonLinks(createSynthLinks());
+        print(node);
+        // TODO: assert structure of JSON created so we know we got it right
+
+    }
 }