Included connect point port number in definition of UiLinkId.
Added dumpString() to ModelCache / UiTopology.
Added more unit tests for ModelCache.

Change-Id: I842bb418b25cc901bd12bc28c6660c836f7235bc
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiDevice.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiDevice.java
index bb59752..3ed9da0 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiDevice.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiDevice.java
@@ -87,6 +87,16 @@
     }
 
     /**
+     * Returns the identifier of the region to which this device belongs.
+     * This will be null if the device does not belong to any region.
+     *
+     * @return region identity
+     */
+    public RegionId regionId() {
+        return regionId;
+    }
+
+    /**
      * Returns the UI region to which this device belongs.
      *
      * @return the UI region
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiLink.java
index b386c5e..916944c 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiLink.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiLink.java
@@ -177,4 +177,42 @@
         edgeLink = elink;
         edgeDevice = elink.hostLocation().deviceId();
     }
+
+
+    /**
+     * Returns the identity of device A.
+     *
+     * @return device A ID
+     */
+    public DeviceId deviceA() {
+        return deviceA;
+    }
+
+    /**
+     * Returns the identity of device B.
+     *
+     * @return device B ID
+     */
+    public DeviceId deviceB() {
+        return deviceB;
+    }
+
+    /**
+     * Returns backing link from A to B.
+     *
+     * @return backing link A to B
+     */
+    public Link linkAtoB() {
+        return linkAtoB;
+    }
+
+    /**
+     * Returns backing link from B to A.
+     *
+     * @return backing link B to A
+     */
+    public Link linkBtoA() {
+        return linkBtoA;
+    }
+
 }
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 070052d..e530db4 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
@@ -19,6 +19,7 @@
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.ElementId;
 import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
 
 /**
  * A canonical representation of an identifier for {@link UiLink}s.
@@ -33,10 +34,14 @@
         B_TO_A
     }
 
-    private static final String ID_DELIMITER = "~";
+    private static final String CP_DELIMITER = "~";
+    private static final String ID_PORT_DELIMITER = "/";
 
     private final ElementId idA;
+    private final PortNumber portA;
     private final ElementId idB;
+    private final PortNumber portB;
+
     private final String idStr;
 
     /**
@@ -46,13 +51,18 @@
      * underlying link.
      *
      * @param a first element ID
+     * @param pa first element port
      * @param b second element ID
+     * @param pb second element port
      */
-    private UiLinkId(ElementId a, ElementId b) {
+    private UiLinkId(ElementId a, PortNumber pa, ElementId b, PortNumber pb) {
         idA = a;
+        portA = pa;
         idB = b;
+        portB = pb;
 
-        idStr = a.toString() + ID_DELIMITER + b.toString();
+        idStr = a + ID_PORT_DELIMITER + pa + CP_DELIMITER +
+                b + ID_PORT_DELIMITER + pb;
     }
 
     @Override
@@ -70,6 +80,15 @@
     }
 
     /**
+     * Returns the port of the first element.
+     *
+     * @return first element port
+     */
+    public PortNumber portA() {
+        return portA;
+    }
+
+    /**
      * Returns the identifier of the second element.
      *
      * @return second element identity
@@ -78,6 +97,15 @@
         return idB;
     }
 
+    /**
+     * Returns the port of the second element.
+     *
+     * @return second element port
+     */
+    public PortNumber portB() {
+        return portB;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -127,13 +155,10 @@
 
         ElementId srcId = src.elementId();
         ElementId dstId = dst.elementId();
-        if (srcId == null || dstId == null) {
-            throw new NullPointerException("null element ID in connect point: " + link);
-        }
 
         // canonicalize
         int comp = srcId.toString().compareTo(dstId.toString());
-        return comp <= 0 ? new UiLinkId(srcId, dstId)
-                : new UiLinkId(dstId, srcId);
+        return comp <= 0 ? new UiLinkId(srcId, src.port(), dstId, dst.port())
+                : new UiLinkId(dstId, dst.port(), srcId, src.port());
     }
 }
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
index 0bcaa58..9776216 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
@@ -16,6 +16,7 @@
 
 package org.onosproject.ui.model.topo;
 
+import com.google.common.collect.ImmutableSet;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.HostId;
 import org.onosproject.net.region.Region;
@@ -117,6 +118,15 @@
     }
 
     /**
+     * Returns the set of device identifiers for this region.
+     *
+     * @return device identifiers for this region
+     */
+    public Set<DeviceId> deviceIds() {
+        return ImmutableSet.copyOf(deviceIds);
+    }
+
+    /**
      * Returns the devices in this region.
      *
      * @return the devices in this region
@@ -126,6 +136,15 @@
     }
 
     /**
+     * Returns the set of host identifiers for this region.
+     *
+     * @return host identifiers for this region
+     */
+    public Set<HostId> hostIds() {
+        return ImmutableSet.copyOf(hostIds);
+    }
+
+    /**
      * Returns the hosts in this region.
      *
      * @return the hosts in this region
@@ -135,6 +154,15 @@
     }
 
     /**
+     * Returns the set of link identifiers for this region.
+     *
+     * @return link identifiers for this region
+     */
+    public Set<UiLinkId> linkIds() {
+        return ImmutableSet.copyOf(uiLinkIds);
+    }
+
+    /**
      * Returns the links in this region.
      *
      * @return the links in this region
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 3a11fcf..9555e3a 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
@@ -35,6 +35,10 @@
  */
 public class UiTopology extends UiElement {
 
+    private static final String INDENT_1 = "  ";
+    private static final String INDENT_2 = "    ";
+    private static final String EOL = String.format("%n");
+
     private static final String E_UNMAPPED =
             "Attempting to retrieve unmapped {}: {}";
 
@@ -133,15 +137,6 @@
     }
 
     /**
-     * Returns the number of regions configured in the topology.
-     *
-     * @return number of regions
-     */
-    public int regionCount() {
-        return regionLookup.size();
-    }
-
-    /**
      * Adds the given region to the topology model.
      *
      * @param uiRegion region to add
@@ -156,7 +151,19 @@
      * @param uiRegion region to remove
      */
     public void remove(UiRegion uiRegion) {
-        regionLookup.remove(uiRegion.id());
+        UiRegion r = regionLookup.remove(uiRegion.id());
+        if (r != null) {
+            r.destroy();
+        }
+    }
+
+    /**
+     * Returns the number of regions configured in the topology.
+     *
+     * @return number of regions
+     */
+    public int regionCount() {
+        return regionLookup.size();
     }
 
     /**
@@ -192,6 +199,15 @@
     }
 
     /**
+     * Returns the number of devices configured in the topology.
+     *
+     * @return number of devices
+     */
+    public int deviceCount() {
+        return deviceLookup.size();
+    }
+
+    /**
      * Returns the link with the specified identifier, or null if no such
      * link exists.
      *
@@ -217,13 +233,22 @@
      * @param uiLink link to remove
      */
     public void remove(UiLink uiLink) {
-        UiLink link = linkLookup.get(uiLink.id());
+        UiLink link = linkLookup.remove(uiLink.id());
         if (link != null) {
             link.destroy();
         }
     }
 
     /**
+     * Returns the number of links configured in the topology.
+     *
+     * @return number of links
+     */
+    public int linkCount() {
+        return linkLookup.size();
+    }
+
+    /**
      * Returns the host with the specified identifier, or null if no such
      * host exists.
      *
@@ -255,6 +280,16 @@
         }
     }
 
+    /**
+     * Returns the number of hosts configured in the topology.
+     *
+     * @return number of hosts
+     */
+    public int hostCount() {
+        return hostLookup.size();
+    }
+
+
     // ==
     // package private methods for supporting linkage amongst topology entities
     // ==
@@ -316,4 +351,42 @@
         return uiLinks;
     }
 
+    /**
+     * Returns a detailed (multi-line) string showing the contents of the
+     * topology.
+     *
+     * @return detailed string
+     */
+    public String dumpString() {
+        StringBuilder sb = new StringBuilder("Topology:").append(EOL);
+
+        sb.append(INDENT_1).append("Cluster Members").append(EOL);
+        for (UiClusterMember m : cnodeLookup.values()) {
+            sb.append(INDENT_2).append(m).append(EOL);
+        }
+
+        sb.append(INDENT_1).append("Regions").append(EOL);
+        for (UiRegion r : regionLookup.values()) {
+            sb.append(INDENT_2).append(r).append(EOL);
+        }
+
+        sb.append(INDENT_1).append("Devices").append(EOL);
+        for (UiDevice d : deviceLookup.values()) {
+            sb.append(INDENT_2).append(d).append(EOL);
+        }
+
+        sb.append(INDENT_1).append("Hosts").append(EOL);
+        for (UiHost h : hostLookup.values()) {
+            sb.append(INDENT_2).append(h).append(EOL);
+        }
+
+        sb.append(INDENT_1).append("Links").append(EOL);
+        for (UiLink link : linkLookup.values()) {
+            sb.append(INDENT_2).append(link).append(EOL);
+        }
+        sb.append("------").append(EOL);
+
+        return sb.toString();
+    }
+
 }
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 51389d1..e129e26 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
@@ -26,6 +26,7 @@
 import org.onosproject.ui.model.AbstractUiModelTest;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.onosproject.net.DeviceId.deviceId;
 import static org.onosproject.net.PortNumber.portNumber;
 
@@ -34,28 +35,34 @@
  */
 public class UiLinkIdTest extends AbstractUiModelTest {
 
-
-    private static final ProviderId PROVIDER_ID = ProviderId.NONE;
-
     private static final DeviceId DEV_X = deviceId("device-X");
     private static final DeviceId DEV_Y = deviceId("device-Y");
     private static final PortNumber P1 = portNumber(1);
     private static final PortNumber P2 = portNumber(2);
+    private static final PortNumber P3 = portNumber(3);
 
-    private static final ConnectPoint CP_X = new ConnectPoint(DEV_X, P1);
-    private static final ConnectPoint CP_Y = new ConnectPoint(DEV_Y, P2);
+    private static final ConnectPoint CP_X1 = new ConnectPoint(DEV_X, P1);
+    private static final ConnectPoint CP_Y2 = new ConnectPoint(DEV_Y, P2);
+    private static final ConnectPoint CP_Y3 = new ConnectPoint(DEV_Y, P3);
 
-    private static final Link LINK_X_TO_Y = DefaultLink.builder()
+    private static final Link LINK_X1_TO_Y2 = DefaultLink.builder()
             .providerId(ProviderId.NONE)
-            .src(CP_X)
-            .dst(CP_Y)
+            .src(CP_X1)
+            .dst(CP_Y2)
             .type(Link.Type.DIRECT)
             .build();
 
-    private static final Link LINK_Y_TO_X = DefaultLink.builder()
+    private static final Link LINK_Y2_TO_X1 = DefaultLink.builder()
             .providerId(ProviderId.NONE)
-            .src(CP_Y)
-            .dst(CP_X)
+            .src(CP_Y2)
+            .dst(CP_X1)
+            .type(Link.Type.DIRECT)
+            .build();
+
+    private static final Link LINK_X1_TO_Y3 = DefaultLink.builder()
+            .providerId(ProviderId.NONE)
+            .src(CP_X1)
+            .dst(CP_Y3)
             .type(Link.Type.DIRECT)
             .build();
 
@@ -63,10 +70,20 @@
     @Test
     public void canonical() {
         title("canonical");
-        UiLinkId one = UiLinkId.uiLinkId(LINK_X_TO_Y);
-        UiLinkId two = UiLinkId.uiLinkId(LINK_Y_TO_X);
+        UiLinkId one = UiLinkId.uiLinkId(LINK_X1_TO_Y2);
+        UiLinkId two = UiLinkId.uiLinkId(LINK_Y2_TO_X1);
         print("link one: %s", one);
         print("link two: %s", two);
         assertEquals("not equiv", one, two);
     }
+
+    @Test
+    public void sameDevsDiffPorts() {
+        title("sameDevsDiffPorts");
+        UiLinkId one = UiLinkId.uiLinkId(LINK_X1_TO_Y2);
+        UiLinkId other = UiLinkId.uiLinkId(LINK_X1_TO_Y3);
+        print("link one: %s", one);
+        print("link other: %s", other);
+        assertNotEquals("equiv?", one, other);
+    }
 }