Implemented initial loading of ModelCache.
Created UiLinkId to canonicalize identifiers for UI links, based on src and dst elements.
Added idAsString() and name() methods to UiElement.
Added toString() to UiDevice, UiLink, UiHost.
Created Mock services for testing.

Change-Id: I4d27110e5aca08f29bb719f17e9ec65d6786e2c8
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 420768f..a9981e7 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
@@ -22,26 +22,48 @@
 import org.onosproject.event.EventDispatcher;
 import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.EdgeLink;
 import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
 import org.onosproject.net.Link;
 import org.onosproject.net.region.Region;
+import org.onosproject.net.region.RegionId;
 import org.onosproject.ui.model.ServiceBundle;
 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.UiLinkId;
+import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiTopology;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Set;
+
+import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
 import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.CLUSTER_MEMBER_ADDED_OR_UPDATED;
 import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.CLUSTER_MEMBER_REMOVED;
 import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.DEVICE_ADDED_OR_UPDATED;
 import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.DEVICE_REMOVED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.HOST_ADDED_OR_UPDATED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.HOST_MOVED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.HOST_REMOVED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.LINK_ADDED_OR_UPDATED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.LINK_REMOVED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.REGION_ADDED_OR_UPDATED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.REGION_REMOVED;
+import static org.onosproject.ui.model.topo.UiLinkId.uiLinkId;
 
 /**
  * UI Topology Model cache.
  */
 class ModelCache {
 
+    private static final String E_NO_ELEMENT = "Tried to remove non-member {}: {}";
+
     private static final Logger log = LoggerFactory.getLogger(ModelCache.class);
 
     private final ServiceBundle services;
@@ -58,123 +80,311 @@
         return "ModelCache{" + uiTopology + "}";
     }
 
-    /**
-     * Clear our model.
-     */
+    private void postEvent(UiModelEvent.Type type, UiElement subject) {
+        dispatcher.post(new UiModelEvent(type, subject));
+    }
+
     void clear() {
         uiTopology.clear();
     }
 
     /**
-     * Create our internal model of the global topology.
+     * Create our internal model of the global topology. An assumption we are
+     * making is that the topology is empty to start.
      */
     void load() {
-        // TODO - implement loading of initial state
-//        loadClusterMembers();
-//        loadRegions();
-//        loadDevices();
-//        loadHosts();
-//        loadLinks();
+        loadClusterMembers();
+        loadRegions();
+        loadDevices();
+        loadLinks();
+        loadHosts();
     }
 
 
-    /**
-     * Updates the model (adds a new instance if necessary) with the given
-     * controller node information.
-     *
-     * @param cnode controller node to be added/updated
-     */
+    // === CLUSTER MEMBERS
+
+    private UiClusterMember addNewClusterMember(ControllerNode n) {
+        UiClusterMember member = new UiClusterMember(uiTopology, n);
+        uiTopology.add(member);
+        return member;
+    }
+
+    private void updateClusterMember(UiClusterMember member) {
+        ControllerNode.State state = services.cluster().getState(member.id());
+        member.setState(state);
+        member.setMastership(services.mastership().getDevicesOf(member.id()));
+        // NOTE: 'UI-attached' is session-based data, not global, so will
+        //       be set elsewhere
+    }
+
+    private void loadClusterMembers() {
+        for (ControllerNode n : services.cluster().getNodes()) {
+            UiClusterMember member = addNewClusterMember(n);
+            updateClusterMember(member);
+        }
+    }
+
+    // invoked from UiSharedTopologyModel cluster event listener
     void addOrUpdateClusterMember(ControllerNode cnode) {
         NodeId id = cnode.id();
         UiClusterMember member = uiTopology.findClusterMember(id);
         if (member == null) {
-            member = new UiClusterMember(cnode);
-            uiTopology.add(member);
+            member = addNewClusterMember(cnode);
         }
+        updateClusterMember(member);
 
-        // inject computed data about the cluster node, into the model object
-        ControllerNode.State state = services.cluster().getState(id);
-        member.setState(state);
-        member.setDeviceCount(services.mastership().getDevicesOf(id).size());
-        // NOTE: UI-attached is session-based data, not global
-
-        dispatcher.post(new UiModelEvent(CLUSTER_MEMBER_ADDED_OR_UPDATED, member));
+        postEvent(CLUSTER_MEMBER_ADDED_OR_UPDATED, member);
     }
 
-    /**
-     * Removes from the model the specified controller node.
-     *
-     * @param cnode controller node to be removed
-     */
+    // package private for unit test access
+    UiClusterMember accessClusterMember(NodeId id) {
+        return uiTopology.findClusterMember(id);
+    }
+
+    // invoked from UiSharedTopologyModel cluster event listener
     void removeClusterMember(ControllerNode cnode) {
         NodeId id = cnode.id();
         UiClusterMember member = uiTopology.findClusterMember(id);
         if (member != null) {
             uiTopology.remove(member);
-            dispatcher.post(new UiModelEvent(CLUSTER_MEMBER_REMOVED, member));
+            postEvent(CLUSTER_MEMBER_REMOVED, member);
         } else {
-            log.warn("Tried to remove non-member cluster node {}", id);
+            log.warn(E_NO_ELEMENT, "cluster node", id);
         }
     }
 
+
+    // === MASTERSHIP CHANGES
+
+    // invoked from UiSharedTopologyModel mastership listener
     void updateMasterships(DeviceId deviceId, RoleInfo roleInfo) {
+        // To think about:: do we need to store mastership info?
+        //  or can we rely on looking it up live?
         // TODO: store the updated mastership information
         // TODO: post event
     }
 
+
+    // === REGIONS
+
+    private UiRegion addNewRegion(Region r) {
+        UiRegion region = new UiRegion(uiTopology, r);
+        uiTopology.add(region);
+        return region;
+    }
+
+    private void updateRegion(UiRegion region) {
+        Set<DeviceId> devs = services.region().getRegionDevices(region.id());
+        region.reconcileDevices(devs);
+    }
+
+    private void loadRegions() {
+        for (Region r : services.region().getRegions()) {
+            UiRegion region = addNewRegion(r);
+            updateRegion(region);
+        }
+    }
+
+    // invoked from UiSharedTopologyModel region listener
     void addOrUpdateRegion(Region region) {
-        // TODO: find or create region assoc. with parameter
-        // TODO: post event
+        RegionId id = region.id();
+        UiRegion uiRegion = uiTopology.findRegion(id);
+        if (uiRegion == null) {
+            uiRegion = addNewRegion(region);
+        }
+        updateRegion(uiRegion);
+
+        postEvent(REGION_ADDED_OR_UPDATED, uiRegion);
     }
 
+    // invoked from UiSharedTopologyModel region listener
     void removeRegion(Region region) {
-        // TODO: find region assoc. with parameter; remove from model
-        // TODO: post event
+        RegionId id = region.id();
+        UiRegion uiRegion = uiTopology.findRegion(id);
+        if (uiRegion != null) {
+            uiTopology.remove(uiRegion);
+            postEvent(REGION_REMOVED, uiRegion);
+        } else {
+            log.warn(E_NO_ELEMENT, "region", id);
+        }
     }
 
+
+    // === DEVICES
+
+    private UiDevice addNewDevice(Device d) {
+        UiDevice device = new UiDevice(uiTopology, d);
+        uiTopology.add(device);
+        return device;
+    }
+
+    private void updateDevice(UiDevice device) {
+        device.setRegionId(services.region().getRegionForDevice(device.id()).id());
+    }
+
+    private void loadDevices() {
+        for (Device d : services.device().getDevices()) {
+            UiDevice device = addNewDevice(d);
+            updateDevice(device);
+        }
+    }
+
+    // invoked from UiSharedTopologyModel device listener
     void addOrUpdateDevice(Device device) {
-        // TODO: find or create device assoc. with parameter
-        // FIXME
-        UiDevice uiDevice = new UiDevice();
+        DeviceId id = device.id();
+        UiDevice uiDevice = uiTopology.findDevice(id);
+        if (uiDevice == null) {
+            uiDevice = addNewDevice(device);
+        }
+        updateDevice(uiDevice);
 
-        // TODO: post the (correct) event
-        dispatcher.post(new UiModelEvent(DEVICE_ADDED_OR_UPDATED, uiDevice));
+        postEvent(DEVICE_ADDED_OR_UPDATED, uiDevice);
     }
 
+    // invoked from UiSharedTopologyModel device listener
     void removeDevice(Device device) {
-        // TODO: get UiDevice associated with the given parameter; remove from model
-        // FIXME
-        UiDevice uiDevice = new UiDevice();
-
-        // TODO: post the (correct) event
-        dispatcher.post(new UiModelEvent(DEVICE_REMOVED, uiDevice));
-
+        DeviceId id = device.id();
+        UiDevice uiDevice = uiTopology.findDevice(id);
+        if (uiDevice != null) {
+            uiTopology.remove(uiDevice);
+            postEvent(DEVICE_REMOVED, uiDevice);
+        } else {
+            log.warn(E_NO_ELEMENT, "device", id);
+        }
     }
 
+
+    // === LINKS
+
+    private UiLink addNewLink(UiLinkId id) {
+        UiLink uiLink = new UiLink(uiTopology, id);
+        uiTopology.add(uiLink);
+        return uiLink;
+    }
+
+    private void updateLink(UiLink uiLink, Link link) {
+        uiLink.attachBackingLink(link);
+    }
+
+    private void loadLinks() {
+        for (Link link : services.link().getLinks()) {
+            UiLinkId id = uiLinkId(link);
+
+            UiLink uiLink = uiTopology.findLink(id);
+            if (uiLink == null) {
+                uiLink = addNewLink(id);
+            }
+            updateLink(uiLink, link);
+        }
+    }
+
+    // invoked from UiSharedTopologyModel link listener
     void addOrUpdateLink(Link link) {
-        // TODO: find ui-link assoc. with parameter; create or update.
-        // TODO: post event
+        UiLinkId id = uiLinkId(link);
+        UiLink uiLink = uiTopology.findLink(id);
+        if (uiLink == null) {
+            uiLink = addNewLink(id);
+        }
+        updateLink(uiLink, link);
+
+        postEvent(LINK_ADDED_OR_UPDATED, uiLink);
     }
 
+    // invoked from UiSharedTopologyModel link listener
     void removeLink(Link link) {
-        // TODO: find ui-link assoc. with parameter; update or remove.
-        // TODO: post event
+        UiLinkId id = uiLinkId(link);
+        UiLink uiLink = uiTopology.findLink(id);
+        if (uiLink != null) {
+            boolean remaining = uiLink.detachBackingLink(link);
+            if (remaining) {
+                postEvent(LINK_ADDED_OR_UPDATED, uiLink);
+            } else {
+                uiTopology.remove(uiLink);
+                postEvent(LINK_REMOVED, uiLink);
+            }
+        } else {
+            log.warn(E_NO_ELEMENT, "link", id);
+        }
     }
 
+
+    // === HOSTS
+
+    private UiHost addNewHost(Host h) {
+        UiHost host = new UiHost(uiTopology, h);
+        uiTopology.add(host);
+
+        UiLink edgeLink = addNewEdgeLink(host);
+        host.setEdgeLinkId(edgeLink.id());
+
+        return host;
+    }
+
+    private void removeOldEdgeLink(UiHost uiHost) {
+        UiLink old = uiTopology.findLink(uiHost.edgeLinkId());
+        if (old != null) {
+            uiTopology.remove(old);
+        }
+    }
+
+    private UiLink addNewEdgeLink(UiHost uiHost) {
+        EdgeLink elink = createEdgeLink(uiHost.backingHost(), true);
+        UiLinkId elinkId = UiLinkId.uiLinkId(elink);
+        UiLink uiLink = addNewLink(elinkId);
+        uiLink.attachEdgeLink(elink);
+        return uiLink;
+    }
+
+    private void updateHost(UiHost uiHost, Host h) {
+        removeOldEdgeLink(uiHost);
+        HostLocation hloc = h.location();
+        uiHost.setLocation(hloc.deviceId(), hloc.port());
+        addNewEdgeLink(uiHost);
+    }
+
+    private void loadHosts() {
+        for (Host h : services.host().getHosts()) {
+            UiHost host = addNewHost(h);
+            updateHost(host, h);
+        }
+    }
+
+    // invoked from UiSharedTopologyModel host listener
     void addOrUpdateHost(Host host) {
-        // TODO: find or create host assoc. with parameter
-        // TODO: post event
+        HostId id = host.id();
+        UiHost uiHost = uiTopology.findHost(id);
+        if (uiHost == null) {
+            uiHost = addNewHost(host);
+        }
+        updateHost(uiHost, host);
+
+        postEvent(HOST_ADDED_OR_UPDATED, uiHost);
     }
 
+    // invoked from UiSharedTopologyModel host listener
     void moveHost(Host host, Host prevHost) {
-        // TODO: process host-move
-        // TODO: post event
+        UiHost uiHost = uiTopology.findHost(prevHost.id());
+        updateHost(uiHost, host);
+
+        postEvent(HOST_MOVED, uiHost);
     }
 
+    // invoked from UiSharedTopologyModel host listener
     void removeHost(Host host) {
-        // TODO: find host assoc. with parameter; remove from model
+        HostId id = host.id();
+        UiHost uiHost = uiTopology.findHost(id);
+        if (uiHost != null) {
+            uiTopology.remove(uiHost);
+            removeOldEdgeLink(uiHost);
+            postEvent(HOST_REMOVED, uiHost);
+        } else {
+            log.warn(E_NO_ELEMENT, "host", id);
+        }
     }
 
+
+    // === CACHE STATISTICS
+
     /**
      * Returns the number of members in the cluster.
      *
@@ -185,7 +395,7 @@
     }
 
     /**
-     * Returns the number of regions configured in the topology.
+     * Returns the number of regions in the topology.
      *
      * @return number of regions
      */
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
index 1a2c66c..144b293 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
@@ -32,9 +32,17 @@
         CLUSTER_MEMBER_ADDED_OR_UPDATED,
         CLUSTER_MEMBER_REMOVED,
 
+        REGION_ADDED_OR_UPDATED,
+        REGION_REMOVED,
+
         DEVICE_ADDED_OR_UPDATED,
         DEVICE_REMOVED,
 
-        // TODO...
+        LINK_ADDED_OR_UPDATED,
+        LINK_REMOVED,
+
+        HOST_ADDED_OR_UPDATED,
+        HOST_MOVED,
+        HOST_REMOVED
     }
 }
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractTopoModelTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractTopoModelTest.java
index 6fbb443..98fc07d 100644
--- a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractTopoModelTest.java
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractTopoModelTest.java
@@ -16,38 +16,241 @@
 
 package org.onosproject.ui.impl.topo.model;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
 import org.onosproject.cluster.ClusterService;
 import org.onosproject.cluster.ClusterServiceAdapter;
 import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.DefaultControllerNode;
 import org.onosproject.cluster.NodeId;
+import org.onosproject.cluster.RoleInfo;
 import org.onosproject.mastership.MastershipService;
 import org.onosproject.mastership.MastershipServiceAdapter;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultDevice;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
 import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.device.DeviceServiceAdapter;
 import org.onosproject.net.flow.FlowRuleService;
 import org.onosproject.net.host.HostService;
+import org.onosproject.net.host.HostServiceAdapter;
 import org.onosproject.net.intent.IntentService;
 import org.onosproject.net.link.LinkService;
+import org.onosproject.net.link.LinkServiceAdapter;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.net.region.DefaultRegion;
+import org.onosproject.net.region.Region;
+import org.onosproject.net.region.RegionId;
+import org.onosproject.net.region.RegionListener;
 import org.onosproject.net.region.RegionService;
 import org.onosproject.ui.impl.AbstractUiImplTest;
 import org.onosproject.ui.model.ServiceBundle;
 
+import java.util.ArrayList;
+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 static org.onosproject.cluster.NodeId.nodeId;
 import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.HostId.hostId;
+import static org.onosproject.net.PortNumber.portNumber;
 
 /**
  * Base class for model test classes.
  */
 abstract class AbstractTopoModelTest extends AbstractUiImplTest {
 
+    /*
+      Our mock environment:
+
+      Three controllers: C1, C2, C3
+
+      Nine devices: D1 .. D9
+
+             D4 ---+              +--- D7
+                   |              |
+            D5 --- D1 --- D2 --- D3 --- D8
+                   |              |
+             D6 ---+              +--- D9
+
+      Twelve hosts (two per D4 ... D9)  H4a, H4b, H5a, H5b, ...
+
+      Regions:
+        R1 : D1, D2, D3
+        R2 : D4, D5, D6
+        R3 : D7, D8, D9
+
+      Mastership:
+        C1 : D1, D2, D3
+        C2 : D4, D5, D6
+        C3 : D7, D8, D9
+
+      Roles: (backups)
+        C1 -> C2, C3
+        C2 -> C1, C3
+        C3 -> C1, C2
+     */
+
+    protected static final String C1 = "C1";
+    protected static final String C2 = "C2";
+    protected static final String C3 = "C3";
+
+    protected static final NodeId CNID_1 = nodeId(C1);
+    protected static final NodeId CNID_2 = nodeId(C2);
+    protected static final NodeId CNID_3 = nodeId(C3);
+
+    protected static final ControllerNode CNODE_1 = cnode(CNID_1, "10.0.0.1");
+    protected static final ControllerNode CNODE_2 = cnode(CNID_2, "10.0.0.2");
+    protected static final ControllerNode CNODE_3 = cnode(CNID_3, "10.0.0.3");
+
+    protected static final String R1 = "R1";
+    protected static final String R2 = "R2";
+    protected static final String R3 = "R3";
+
+    protected static final Set<NodeId> SET_C1 = ImmutableSet.of(CNID_1);
+    protected static final Set<NodeId> SET_C2 = ImmutableSet.of(CNID_2);
+    protected static final Set<NodeId> SET_C3 = ImmutableSet.of(CNID_3);
+
+    protected static final Region REGION_1 =
+            region(R1, Region.Type.METRO, ImmutableList.of(SET_C1, SET_C2));
+    protected static final Region REGION_2 =
+            region(R2, Region.Type.CAMPUS, ImmutableList.of(SET_C2, SET_C1));
+    protected static final Region REGION_3 =
+            region(R3, Region.Type.CAMPUS, ImmutableList.of(SET_C3, SET_C1));
+
+    protected static final Set<Region> REGION_SET =
+            ImmutableSet.of(REGION_1, REGION_2, REGION_3);
+
+    protected static final String D1 = "D1";
+    protected static final String D2 = "D2";
+    protected static final String D3 = "D3";
+    protected static final String D4 = "D4";
+    protected static final String D5 = "D5";
+    protected static final String D6 = "D6";
+    protected static final String D7 = "D7";
+    protected static final String D8 = "D8";
+    protected static final String D9 = "D9";
+
+    protected static final String MFR = "Mfr";
+    protected static final String HW = "h/w";
+    protected static final String SW = "s/w";
+    protected static final String SERIAL = "ser123";
+
+    protected static final DeviceId DEVID_1 = deviceId(D1);
+    protected static final DeviceId DEVID_2 = deviceId(D2);
+    protected static final DeviceId DEVID_3 = deviceId(D3);
+    protected static final DeviceId DEVID_4 = deviceId(D4);
+    protected static final DeviceId DEVID_5 = deviceId(D5);
+    protected static final DeviceId DEVID_6 = deviceId(D6);
+    protected static final DeviceId DEVID_7 = deviceId(D7);
+    protected static final DeviceId DEVID_8 = deviceId(D8);
+    protected static final DeviceId DEVID_9 = deviceId(D9);
+
+    protected static final Device DEV_1 = device(D1);
+    protected static final Device DEV_2 = device(D2);
+    protected static final Device DEV_3 = device(D3);
+    protected static final Device DEV_4 = device(D4);
+    protected static final Device DEV_5 = device(D5);
+    protected static final Device DEV_6 = device(D6);
+    protected static final Device DEV_7 = device(D7);
+    protected static final Device DEV_8 = device(D8);
+    protected static final Device DEV_9 = device(D9);
+
+    protected static final List<Device> ALL_DEVS =
+            ImmutableList.of(
+                    DEV_1, DEV_2, DEV_3,
+                    DEV_4, DEV_5, DEV_6,
+                    DEV_7, DEV_8, DEV_9
+            );
+
+    private static final Set<DeviceId> DEVS_TRUNK =
+            ImmutableSet.of(DEVID_1, DEVID_2, DEVID_3);
+
+    private static final Set<DeviceId> DEVS_LEFT =
+            ImmutableSet.of(DEVID_4, DEVID_5, DEVID_6);
+
+    private static final Set<DeviceId> DEVS_RIGHT =
+            ImmutableSet.of(DEVID_7, DEVID_8, DEVID_9);
+
+    private static final String[][] LINK_CONNECT_DATA = {
+            {D1, "12", D2, "21"},
+            {D2, "23", D3, "32"},
+            {D4, "41", D1, "14"},
+            {D5, "51", D1, "15"},
+            {D6, "61", D1, "16"},
+            {D7, "73", D3, "37"},
+            {D8, "83", D3, "38"},
+            {D9, "93", D3, "39"},
+    };
+
+    private static final String HOST_MAC_PREFIX = "aa:00:00:00:00:";
+
+    /**
+     * Returns IP address instance for given string.
+     *
+     * @param s string
+     * @return IP address
+     */
+    protected static IpAddress ip(String s) {
+        return IpAddress.valueOf(s);
+    }
+
+    /**
+     * Returns controller node instance for given ID and IP.
+     *
+     * @param id identifier
+     * @param ip IP address
+     * @return controller node instance
+     */
+    protected static ControllerNode cnode(NodeId id, String ip) {
+        return new DefaultControllerNode(id, ip(ip));
+    }
+
+    /**
+     * Returns a region instance with specified parameters.
+     *
+     * @param id      region id
+     * @param type    region type
+     * @param masters ordered list of master sets
+     * @return region instance
+     */
+    protected static Region region(String id, Region.Type type,
+                                   List<Set<NodeId>> masters) {
+        return new DefaultRegion(RegionId.regionId(id), "Region-" + id,
+                type, masters);
+    }
+
+    /**
+     * Returns device with given ID.
+     *
+     * @param id device ID
+     * @return device instance
+     */
+    protected static Device device(String id) {
+        return new DefaultDevice(ProviderId.NONE, deviceId(id),
+                Device.Type.SWITCH, MFR, HW, SW, SERIAL, null);
+    }
+
     /**
      * Returns canned results.
+     * <p>
      * At some future point, we may make this "programmable", so that
-     * it returns certain values based on element IDs etc.
+     * its state can be changed over the course of a unit test.
      */
     protected static final ServiceBundle MOCK_SERVICES =
             new ServiceBundle() {
@@ -63,22 +266,22 @@
 
                 @Override
                 public RegionService region() {
-                    return null;
+                    return MOCK_REGION;
                 }
 
                 @Override
                 public DeviceService device() {
-                    return null;
+                    return MOCK_DEVICE;
                 }
 
                 @Override
                 public LinkService link() {
-                    return null;
+                    return MOCK_LINK;
                 }
 
                 @Override
                 public HostService host() {
-                    return null;
+                    return MOCK_HOST;
                 }
 
                 @Override
@@ -94,64 +297,286 @@
 
     private static final ClusterService MOCK_CLUSTER = new MockClusterService();
     private static final MastershipService MOCK_MASTER = new MockMasterService();
-    // TODO: fill out as necessary
+    private static final RegionService MOCK_REGION = new MockRegionService();
+    private static final DeviceService MOCK_DEVICE = new MockDeviceService();
+    private static final LinkService MOCK_LINK = new MockLinkService();
+    private static final HostService MOCK_HOST = new MockHostService();
 
-    /*
-      Our mock environment:
-
-      Three controllers: C1, C2, C3
-
-      Nine devices: D1 .. D9
-
-             D4 ---+              +--- D7
-                   |              |
-            D5 --- D1 --- D2 --- D3 --- D8
-                   |              |
-             D6 ---+              +--- D9
-
-      Twelve hosts (two per D4 ... D9)  H41, H42, H51, H52, ...
-
-      Regions:
-        R1 : D1, D2, D3
-        R2 : D4, D5, D6
-        R3 : D7, D8, D9
-
-      Mastership:
-        C1 : D1, D2, D3
-        C2 : D4, D5, D6
-        C3 : D7, D8, D9
-     */
 
 
     private static class MockClusterService extends ClusterServiceAdapter {
+        private final Map<NodeId, ControllerNode> nodes = new HashMap<>();
         private final Map<NodeId, ControllerNode.State> states = new HashMap<>();
 
+        MockClusterService() {
+            nodes.put(CNODE_1.id(), CNODE_1);
+            nodes.put(CNODE_2.id(), CNODE_2);
+            nodes.put(CNODE_3.id(), CNODE_3);
+
+            states.put(CNODE_1.id(), ControllerNode.State.READY);
+            states.put(CNODE_2.id(), ControllerNode.State.ACTIVE);
+            states.put(CNODE_3.id(), ControllerNode.State.ACTIVE);
+        }
+
+        @Override
+        public Set<ControllerNode> getNodes() {
+            return ImmutableSet.copyOf(nodes.values());
+        }
+
+        @Override
+        public ControllerNode getNode(NodeId nodeId) {
+            return nodes.get(nodeId);
+        }
 
         @Override
         public ControllerNode.State getState(NodeId nodeId) {
-            // For now, a hardcoded state of ACTIVE (but not READY)
-            // irrespective of the node ID.
-            return ControllerNode.State.ACTIVE;
+            return states.get(nodeId);
         }
     }
 
-    protected static final DeviceId D1_ID = deviceId("D1");
-    protected static final DeviceId D2_ID = deviceId("D2");
-    protected static final DeviceId D3_ID = deviceId("D3");
-    protected static final DeviceId D4_ID = deviceId("D4");
-    protected static final DeviceId D5_ID = deviceId("D5");
-    protected static final DeviceId D6_ID = deviceId("D6");
-    protected static final DeviceId D7_ID = deviceId("D7");
-    protected static final DeviceId D8_ID = deviceId("D8");
-    protected static final DeviceId D9_ID = deviceId("D9");
 
     private static class MockMasterService extends MastershipServiceAdapter {
+        private final Map<NodeId, Set<DeviceId>> masterOf = new HashMap<>();
+
+        MockMasterService() {
+            masterOf.put(CNODE_1.id(), DEVS_TRUNK);
+            masterOf.put(CNODE_2.id(), DEVS_LEFT);
+            masterOf.put(CNODE_3.id(), DEVS_RIGHT);
+        }
+
+        @Override
+        public NodeId getMasterFor(DeviceId deviceId) {
+            if (DEVS_TRUNK.contains(deviceId)) {
+                return CNID_1;
+            }
+            if (DEVS_LEFT.contains(deviceId)) {
+                return CNID_2;
+            }
+            if (DEVS_RIGHT.contains(deviceId)) {
+                return CNID_3;
+            }
+            return null;
+        }
+
         @Override
         public Set<DeviceId> getDevicesOf(NodeId nodeId) {
-            // For now, a hard coded set of two device IDs
-            // irrespective of the node ID.
-            return ImmutableSet.of(D1_ID, D2_ID);
+            return masterOf.get(nodeId);
         }
+
+        @Override
+        public RoleInfo getNodesFor(DeviceId deviceId) {
+            NodeId master = null;
+            List<NodeId> backups = new ArrayList<>();
+
+            if (DEVS_TRUNK.contains(deviceId)) {
+                master = CNID_1;
+                backups.add(CNID_2);
+                backups.add(CNID_3);
+            } else if (DEVS_LEFT.contains(deviceId)) {
+                master = CNID_2;
+                backups.add(CNID_1);
+                backups.add(CNID_3);
+            } else if (DEVS_RIGHT.contains(deviceId)) {
+                master = CNID_3;
+                backups.add(CNID_1);
+                backups.add(CNID_2);
+            }
+            return new RoleInfo(master, backups);
+        }
+    }
+
+    // TODO: consider implementing RegionServiceAdapter and extending that here
+    private static class MockRegionService implements RegionService {
+
+        private final Map<RegionId, Region> lookup = new HashMap<>();
+
+        MockRegionService() {
+            lookup.put(REGION_1.id(), REGION_1);
+            lookup.put(REGION_2.id(), REGION_2);
+            lookup.put(REGION_3.id(), REGION_3);
+        }
+
+        @Override
+        public Set<Region> getRegions() {
+            return REGION_SET;
+        }
+
+        @Override
+        public Region getRegion(RegionId regionId) {
+            return lookup.get(regionId);
+        }
+
+        @Override
+        public Region getRegionForDevice(DeviceId deviceId) {
+            if (DEVS_TRUNK.contains(deviceId)) {
+                return REGION_1;
+            }
+            if (DEVS_LEFT.contains(deviceId)) {
+                return REGION_2;
+            }
+            if (DEVS_RIGHT.contains(deviceId)) {
+                return REGION_3;
+            }
+            return null;
+        }
+
+        @Override
+        public Set<DeviceId> getRegionDevices(RegionId regionId) {
+            if (REGION_1.id().equals(regionId)) {
+                return DEVS_TRUNK;
+            }
+            if (REGION_2.id().equals(regionId)) {
+                return DEVS_LEFT;
+            }
+            if (REGION_3.id().equals(regionId)) {
+                return DEVS_RIGHT;
+            }
+            return Collections.emptySet();
+        }
+
+        @Override
+        public void addListener(RegionListener listener) {
+        }
+
+        @Override
+        public void removeListener(RegionListener listener) {
+        }
+    }
+
+
+    private static class MockDeviceService extends DeviceServiceAdapter {
+        private final Map<DeviceId, Device> devices = new HashMap<>();
+
+        MockDeviceService() {
+            for (Device dev : ALL_DEVS) {
+                devices.put(dev.id(), dev);
+            }
+        }
+
+        @Override
+        public int getDeviceCount() {
+            return devices.size();
+        }
+
+        @Override
+        public Iterable<Device> getDevices() {
+            return ImmutableList.copyOf(devices.values());
+        }
+
+        @Override
+        public Device getDevice(DeviceId deviceId) {
+            return devices.get(deviceId);
+        }
+
+    }
+
+
+    private static class MockLinkService extends LinkServiceAdapter {
+        private final Set<Link> links = new HashSet<>();
+
+        MockLinkService() {
+            for (String[] linkPair : LINK_CONNECT_DATA) {
+                links.addAll(makeLinks(linkPair));
+            }
+
+        }
+
+        private Set<Link> makeLinks(String[] linkPair) {
+            DeviceId devA = deviceId(linkPair[0]);
+            PortNumber portA = portNumber(Long.valueOf(linkPair[1]));
+            DeviceId devB = deviceId(linkPair[2]);
+            PortNumber portB = portNumber(Long.valueOf(linkPair[3]));
+
+            Link linkA = DefaultLink.builder()
+                    .providerId(ProviderId.NONE)
+                    .type(Link.Type.DIRECT)
+                    .src(new ConnectPoint(devA, portA))
+                    .dst(new ConnectPoint(devB, portB))
+                    .build();
+
+            Link linkB = DefaultLink.builder()
+                    .providerId(ProviderId.NONE)
+                    .type(Link.Type.DIRECT)
+                    .src(new ConnectPoint(devB, portB))
+                    .dst(new ConnectPoint(devA, portA))
+                    .build();
+
+            return ImmutableSet.of(linkA, linkB);
+        }
+
+        @Override
+        public int getLinkCount() {
+            return links.size();
+        }
+
+        @Override
+        public Iterable<Link> getLinks() {
+            return ImmutableSet.copyOf(links);
+        }
+
+        // TODO: possibly fill out other methods if we find the model uses them
+    }
+
+
+    private static class MockHostService extends HostServiceAdapter {
+        private final Map<HostId, Host> hosts = new HashMap<>();
+
+        MockHostService() {
+            for (Device d : ALL_DEVS) {
+                // two hosts per device
+                createHosts(hosts, d);
+            }
+        }
+
+        private void createHosts(Map<HostId, Host> hosts, Device d) {
+            DeviceId deviceId = d.id();
+            String devNum = deviceId.toString().substring(1);
+
+            String ha = devNum + "a";
+            String hb = devNum + "b";
+
+            MacAddress macA = MacAddress.valueOf(HOST_MAC_PREFIX + ha);
+            MacAddress macB = MacAddress.valueOf(HOST_MAC_PREFIX + hb);
+
+            HostId hostA = hostId(String.format("%s/-1", macA));
+            HostId hostB = hostId(String.format("%s/-1", macB));
+
+            PortNumber portA = portNumber(101);
+            PortNumber portB = portNumber(102);
+
+            HostLocation locA = new HostLocation(deviceId, portA, 0);
+            HostLocation locB = new HostLocation(deviceId, portB, 0);
+
+            IpAddress ipA = ip("10." + devNum + ".0.1");
+            IpAddress ipB = ip("10." + devNum + ".0.2");
+
+            Host host = new DefaultHost(ProviderId.NONE,
+                    hostA, macA, VlanId.NONE, locA,
+                    ImmutableSet.of(ipA));
+            hosts.put(hostA, host);
+
+            host = new DefaultHost(ProviderId.NONE,
+                    hostB, macB, VlanId.NONE, locB,
+                    ImmutableSet.of(ipB));
+            hosts.put(hostB, host);
+        }
+
+        @Override
+        public int getHostCount() {
+            return hosts.size();
+        }
+
+        @Override
+        public Iterable<Host> getHosts() {
+            return ImmutableSet.copyOf(hosts.values());
+        }
+
+        @Override
+        public Host getHost(HostId hostId) {
+            return hosts.get(hostId);
+        }
+
+        // TODO: possibly fill out other methods, should the model require them
     }
 
 }
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
index c272087..752f28b 100644
--- a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
@@ -18,16 +18,17 @@
 
 import org.junit.Before;
 import org.junit.Test;
-import org.onlab.packet.IpAddress;
-import org.onosproject.cluster.ControllerNode;
-import org.onosproject.cluster.DefaultControllerNode;
 import org.onosproject.event.Event;
 import org.onosproject.event.EventDispatcher;
+import org.onosproject.net.DeviceId;
 import org.onosproject.ui.impl.topo.model.UiModelEvent.Type;
+import org.onosproject.ui.model.topo.UiClusterMember;
 import org.onosproject.ui.model.topo.UiElement;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.onosproject.cluster.NodeId.nodeId;
 
 /**
@@ -58,22 +59,6 @@
         }
     }
 
-    private static IpAddress ip(String s) {
-        return IpAddress.valueOf(s);
-    }
-
-    private static ControllerNode cnode(String id, String ip) {
-        return new DefaultControllerNode(nodeId(id), ip(ip));
-    }
-
-    private static final String C1 = "C1";
-    private static final String C2 = "C2";
-    private static final String C3 = "C3";
-
-    private static final ControllerNode NODE_1 = cnode(C1, "10.0.0.1");
-    private static final ControllerNode NODE_2 = cnode(C2, "10.0.0.2");
-    private static final ControllerNode NODE_3 = cnode(C3, "10.0.0.3");
-
 
     private final TestEvDisp dispatcher = new TestEvDisp();
 
@@ -99,13 +84,13 @@
         assertEquals("unex # members", 0, cache.clusterMemberCount());
         dispatcher.assertEventCount(0);
 
-        cache.addOrUpdateClusterMember(NODE_1);
+        cache.addOrUpdateClusterMember(CNODE_1);
         print(cache);
         assertEquals("unex # members", 1, cache.clusterMemberCount());
         dispatcher.assertEventCount(1);
         dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C1);
 
-        cache.removeClusterMember(NODE_1);
+        cache.removeClusterMember(CNODE_1);
         print(cache);
         assertEquals("unex # members", 0, cache.clusterMemberCount());
         dispatcher.assertEventCount(2);
@@ -115,14 +100,69 @@
     @Test
     public void createThreeNodeCluster() {
         title("createThreeNodeCluster");
-        cache.addOrUpdateClusterMember(NODE_1);
+        cache.addOrUpdateClusterMember(CNODE_1);
         dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C1);
-        cache.addOrUpdateClusterMember(NODE_2);
+        cache.addOrUpdateClusterMember(CNODE_2);
         dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C2);
-        cache.addOrUpdateClusterMember(NODE_3);
+        cache.addOrUpdateClusterMember(CNODE_3);
         dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C3);
         dispatcher.assertEventCount(3);
         print(cache);
     }
 
+    @Test
+    public void addNodeThenExamineIt() {
+        title("addNodeThenExamineIt");
+        cache.addOrUpdateClusterMember(CNODE_1);
+        dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C1);
+
+        UiClusterMember member = cache.accessClusterMember(nodeId(C1));
+        print(member);
+        // see AbstractUiImplTest Mock Environment for expected values...
+        assertEquals("wrong id str", C1, member.idAsString());
+        assertEquals("wrong id", nodeId(C1), member.id());
+        assertEquals("wrong dev count", 3, member.deviceCount());
+        assertEquals("not online", true, member.isOnline());
+        assertEquals("not ready", true, member.isReady());
+
+        assertMasterOf(member, DEVID_1, DEVID_2, DEVID_3);
+        assertNotMasterOf(member, DEVID_4, DEVID_6, DEVID_9);
+    }
+
+    private void assertMasterOf(UiClusterMember member, DeviceId... ids) {
+        for (DeviceId id : ids) {
+            assertTrue("not master of " + id, member.masterOf(id));
+        }
+    }
+
+    private void assertNotMasterOf(UiClusterMember member, DeviceId... ids) {
+        for (DeviceId id : ids) {
+            assertFalse("? master of " + id, member.masterOf(id));
+        }
+    }
+
+
+    @Test
+    public void addNodeAndDevices() {
+        title("addNodeAndDevices");
+        cache.addOrUpdateClusterMember(CNODE_1);
+        cache.addOrUpdateDevice(DEV_1);
+        cache.addOrUpdateDevice(DEV_2);
+        cache.addOrUpdateDevice(DEV_3);
+        print(cache);
+    }
+
+    @Test
+    public void addRegions() {
+        title("addRegions");
+        cache.addOrUpdateRegion(REGION_1);
+        print(cache);
+    }
+
+    @Test
+    public void load() {
+        title("load");
+        cache.load();
+        print(cache);
+    }
 }