ONOS-4971: Synthetic Link Data -- WIP

- Enhancing UiRegion to capture the hierarchical (parent/child) relationships captured in the UiTopoLayouts.

Change-Id: I152e0d52d4580b14b679f3387402077f16f61e6a
diff --git a/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java b/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java
index 0036521..c6ae247 100644
--- a/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java
+++ b/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java
@@ -15,6 +15,7 @@
  */
 package org.onosproject.ui;
 
+import org.onosproject.net.region.RegionId;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.onosproject.ui.model.topo.UiTopoLayoutId;
 
@@ -57,6 +58,15 @@
     UiTopoLayout getLayout(UiTopoLayoutId layoutId);
 
     /**
+     * Returns the layout which has the backing region identified by
+     * the given region identifier.
+     *
+     * @param regionId region identifier
+     * @return corresponding layout
+     */
+    UiTopoLayout getLayout(RegionId regionId);
+
+    /**
      * Returns the set of peer layouts of the specified layout. That is,
      * those layouts that share the same parent.
      *
diff --git a/core/api/src/main/java/org/onosproject/ui/model/ServiceBundle.java b/core/api/src/main/java/org/onosproject/ui/model/ServiceBundle.java
index 61cf721..6986cf7 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/ServiceBundle.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/ServiceBundle.java
@@ -24,11 +24,20 @@
 import org.onosproject.net.intent.IntentService;
 import org.onosproject.net.link.LinkService;
 import org.onosproject.net.region.RegionService;
+import org.onosproject.ui.UiTopoLayoutService;
 
 /**
  * A bundle of services to pass to elements that might need a reference to them.
  */
 public interface ServiceBundle {
+
+    /**
+     * Reference to a UI Topology Layout service implementation.
+     *
+     * @return layout service
+     */
+    UiTopoLayoutService layout();
+
     /**
      * Reference to a cluster service implementation.
      *
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 c478424..b3185f1 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
@@ -61,6 +61,10 @@
 
     private final Region region;
 
+    // keep track of hierarchy (inferred from UiTopoLayoutService)
+    private RegionId parent;
+    private final Set<RegionId> kids = new HashSet<>();
+
     /**
      * Constructs a UI region, with a reference to the specified backing region.
      *
@@ -103,6 +107,52 @@
         return region == null ? NULL_ID : region.id();
     }
 
+    /**
+     * Returns the identity of the parent region.
+     *
+     * @return parent region ID
+     */
+    public RegionId parent() {
+        return parent;
+    }
+
+    /**
+     * Returns true if this is the root (default) region.
+     *
+     * @return true if root region
+     */
+    public boolean isRoot() {
+        return id().equals(parent);
+    }
+
+    /**
+     * Returns the identities of the child regions.
+     *
+     * @return child region IDs
+     */
+    public Set<RegionId> children() {
+        return ImmutableSet.copyOf(kids);
+    }
+
+    /**
+     * Sets the parent ID for this region.
+     *
+     * @param parentId parent ID
+     */
+    public void setParent(RegionId parentId) {
+        parent = parentId;
+    }
+
+    /**
+     * Sets the children IDs for this region.
+     *
+     * @param children children IDs
+     */
+    public void setChildren(Set<RegionId> children) {
+        kids.clear();
+        kids.addAll(children);
+    }
+
     @Override
     public String idAsString() {
         return id().toString();
@@ -138,6 +188,8 @@
         return toStringHelper(this)
                 .add("id", id())
                 .add("name", name())
+                .add("parent", parent)
+                .add("kids", kids)
                 .add("devices", deviceIds)
                 .add("#hosts", hostIds.size())
                 .add("#links", uiLinkIds.size())
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayout.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayout.java
index 12c9de6..ade86e1 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayout.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayout.java
@@ -68,13 +68,16 @@
     }
 
     /**
-     * Returns the identifier of the backing region. Will be null if the
-     * region is null.
+     * Returns the identifier of the backing region. If this is the default
+     * layout, the null-region ID will be returned, otherwise the ID of the
+     * backing region for this layout will be returned; null in the case that
+     * there is no backing region.
      *
      * @return backing region identifier
      */
     public RegionId regionId() {
-        return region == null ? null : region.id();
+        return isRoot() ? UiRegion.NULL_ID
+                : (region == null ? null : region.id());
     }
 
     /**
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 585e658..708fd18 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
@@ -151,7 +151,8 @@
 
 
     /**
-     * Returns all regions in the model.
+     * Returns all regions in the model (except the
+     * {@link #nullRegion() null region}).
      *
      * @return all regions
      */
@@ -177,7 +178,7 @@
      * @return corresponding UI region
      */
     public UiRegion findRegion(RegionId id) {
-        return regionLookup.get(id);
+        return UiRegion.NULL_ID.equals(id) ? nullRegion() : regionLookup.get(id);
     }
 
     /**
diff --git a/core/api/src/test/java/org/onosproject/ui/model/AbstractUiModelTest.java b/core/api/src/test/java/org/onosproject/ui/model/AbstractUiModelTest.java
index 5037832..15b3870 100644
--- a/core/api/src/test/java/org/onosproject/ui/model/AbstractUiModelTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/model/AbstractUiModelTest.java
@@ -28,6 +28,7 @@
 import org.onosproject.net.link.LinkService;
 import org.onosproject.net.region.RegionService;
 import org.onosproject.ui.AbstractUiTest;
+import org.onosproject.ui.UiTopoLayoutService;
 
 /**
  * Base class for UI model unit tests.
@@ -42,6 +43,11 @@
     protected static final ServiceBundle MOCK_SERVICES =
             new ServiceBundle() {
                 @Override
+                public UiTopoLayoutService layout() {
+                    return null;
+                }
+
+                @Override
                 public ClusterService cluster() {
                     return MOCK_CLUSTER;
                 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoLayoutManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoLayoutManager.java
index fcf4c46..cdd95cf 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoLayoutManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoLayoutManager.java
@@ -24,17 +24,20 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
 import org.onlab.util.KryoNamespace;
+import org.onosproject.net.region.RegionId;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.service.ConsistentMap;
 import org.onosproject.store.service.Serializer;
 import org.onosproject.store.service.StorageService;
 import org.onosproject.ui.UiTopoLayoutService;
+import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.onosproject.ui.model.topo.UiTopoLayoutId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -111,6 +114,18 @@
     }
 
     @Override
+    public UiTopoLayout getLayout(RegionId regionId) {
+        if (regionId == null || regionId.equals(UiRegion.NULL_ID)) {
+            return getRootLayout();
+        }
+
+        List<UiTopoLayout> matchingLayouts = layoutMap.values().stream()
+                .filter(l -> Objects.equals(regionId, l.regionId()))
+                .collect(Collectors.toList());
+        return matchingLayouts.isEmpty() ? null : matchingLayouts.get(0);
+    }
+
+    @Override
     public Set<UiTopoLayout> getPeerLayouts(UiTopoLayoutId layoutId) {
         checkNotNull(layoutId, ID_NULL);
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/cli/ListRegions.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/cli/ListRegions.java
index cdb8bd5..70e0826 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/cli/ListRegions.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/cli/ListRegions.java
@@ -29,6 +29,7 @@
     @Override
     protected void execute() {
         UiSharedTopologyModel model = get(UiSharedTopologyModel.class);
+        print("%s", model.getNullRegion());
         sorted(model.getRegions()).forEach(r -> print("%s", r));
     }
 }
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 4ca6b42..781650d 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
@@ -29,6 +29,7 @@
 import org.onosproject.net.Link;
 import org.onosproject.net.region.Region;
 import org.onosproject.net.region.RegionId;
+import org.onosproject.ui.UiTopoLayoutService;
 import org.onosproject.ui.model.ServiceBundle;
 import org.onosproject.ui.model.topo.UiClusterMember;
 import org.onosproject.ui.model.topo.UiDevice;
@@ -37,6 +38,8 @@
 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.UiTopoLayout;
+import org.onosproject.ui.model.topo.UiTopoLayoutId;
 import org.onosproject.ui.model.topo.UiTopology;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -202,6 +205,34 @@
 
         // Make sure the region object refers to the devices
         region.reconcileDevices(deviceIds);
+
+        fixupContainmentHierarchy(region);
+    }
+
+    private void fixupContainmentHierarchy(UiRegion region) {
+        UiTopoLayoutService ls = services.layout();
+        RegionId regionId = region.id();
+
+        UiTopoLayout layout = ls.getLayout(regionId);
+        if (layout == null) {
+            // no layout backed by this region
+            log.warn("No layout backed by region {}", regionId);
+            return;
+        }
+
+        UiTopoLayoutId layoutId = layout.id();
+
+        if (!layout.isRoot()) {
+            UiTopoLayoutId parentId = layout.parent();
+            UiTopoLayout parentLayout = ls.getLayout(parentId);
+            RegionId parentRegionId = parentLayout.regionId();
+            region.setParent(parentRegionId);
+        }
+
+        Set<UiTopoLayout> kids = ls.getChildren(layoutId);
+        Set<RegionId> kidRegionIds = new HashSet<>(kids.size());
+        kids.forEach(k -> kidRegionIds.add(k.regionId()));
+        region.setChildren(kidRegionIds);
     }
 
     private void loadRegions() {
@@ -478,7 +509,11 @@
     public void refresh() {
         // fix up internal linkages if they aren't correct
 
-        // at the moment, this is making sure devices are in the correct region
+        // make sure regions reflect layout containment hierarchy
+        fixupContainmentHierarchy(uiTopology.nullRegion());
+        uiTopology.allRegions().forEach(this::fixupContainmentHierarchy);
+
+        // make sure devices are in the correct region
         Set<UiDevice> allDevices = uiTopology.allDevices();
 
         services.region().getRegions().forEach(r -> {
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 4a5cc91..a12e34d 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
@@ -60,6 +60,7 @@
 import org.onosproject.net.region.RegionService;
 import org.onosproject.net.statistic.StatisticService;
 import org.onosproject.net.topology.TopologyService;
+import org.onosproject.ui.UiTopoLayoutService;
 import org.onosproject.ui.impl.topo.UiTopoSession;
 import org.onosproject.ui.model.ServiceBundle;
 import org.onosproject.ui.model.topo.UiClusterMember;
@@ -87,6 +88,9 @@
             LoggerFactory.getLogger(UiSharedTopologyModel.class);
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private UiTopoLayoutService layoutService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     private ClusterService clusterService;
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     private MastershipService mastershipService;
@@ -282,6 +286,11 @@
      */
     private class DefaultServiceBundle implements ServiceBundle {
         @Override
+        public UiTopoLayoutService layout() {
+            return layoutService;
+        }
+
+        @Override
         public ClusterService cluster() {
             return clusterService;
         }
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 75a405e..de7ec9a 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
@@ -54,8 +54,11 @@
 import org.onosproject.net.region.RegionId;
 import org.onosproject.net.region.RegionListener;
 import org.onosproject.net.region.RegionService;
+import org.onosproject.ui.UiTopoLayoutService;
 import org.onosproject.ui.impl.AbstractUiImplTest;
 import org.onosproject.ui.model.ServiceBundle;
+import org.onosproject.ui.model.topo.UiTopoLayout;
+import org.onosproject.ui.model.topo.UiTopoLayoutId;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -69,6 +72,7 @@
 import static org.onosproject.net.DeviceId.deviceId;
 import static org.onosproject.net.HostId.hostId;
 import static org.onosproject.net.PortNumber.portNumber;
+import static org.onosproject.ui.model.topo.UiTopoLayoutId.layoutId;
 
 /**
  * Base class for model test classes.
@@ -90,6 +94,12 @@
 
       Twelve hosts (two per D4 ... D9)  H4a, H4b, H5a, H5b, ...
 
+      Layouts:
+        LROOT : (default)
+        +-- L1 : R1
+        +-- L2 : R2
+        +-- L3 : R3
+
       Regions:
         R1 : D1, D2, D3
         R2 : D4, D5, D6
@@ -136,6 +146,27 @@
     protected static final Set<Region> REGION_SET =
             ImmutableSet.of(REGION_1, REGION_2, REGION_3);
 
+    protected static final String LROOT = "LROOT";
+    protected static final String L1 = "L1";
+    protected static final String L2 = "L2";
+    protected static final String L3 = "L3";
+
+    protected static final UiTopoLayout LAYOUT_ROOT = layout(LROOT, null, null);
+    protected static final UiTopoLayout LAYOUT_1 = layout(L1, REGION_1, LROOT);
+    protected static final UiTopoLayout LAYOUT_2 = layout(L2, REGION_2, LROOT);
+    protected static final UiTopoLayout LAYOUT_3 = layout(L3, REGION_3, LROOT);
+
+    protected static final Set<UiTopoLayout> LAYOUT_SET =
+            ImmutableSet.of(LAYOUT_ROOT, LAYOUT_1, LAYOUT_2, LAYOUT_3);
+    protected static final Set<UiTopoLayout> ROOT_KIDS =
+            ImmutableSet.of(LAYOUT_1, LAYOUT_2, LAYOUT_3);
+    protected static final Set<UiTopoLayout> PEERS_OF_1 =
+            ImmutableSet.of(LAYOUT_2, LAYOUT_3);
+    protected static final Set<UiTopoLayout> PEERS_OF_2 =
+            ImmutableSet.of(LAYOUT_1, LAYOUT_3);
+    protected static final Set<UiTopoLayout> PEERS_OF_3 =
+            ImmutableSet.of(LAYOUT_1, LAYOUT_2);
+
     protected static final String D1 = "d1";
     protected static final String D2 = "d2";
     protected static final String D3 = "d3";
@@ -222,6 +253,21 @@
     }
 
     /**
+     * Returns UI topology layout instance with the specified parameters.
+     *
+     * @param layoutId the layout ID
+     * @param region   the backing region
+     * @param parentId the parent layout ID
+     * @return layout instance
+     */
+    protected static UiTopoLayout layout(String layoutId, Region region,
+                                         String parentId) {
+        UiTopoLayoutId pid = parentId == null
+                ? UiTopoLayoutId.DEFAULT_ID : layoutId(parentId);
+        return new UiTopoLayout(layoutId(layoutId), region, pid);
+    }
+
+    /**
      * Returns a region instance with specified parameters.
      *
      * @param id      region id
@@ -255,6 +301,11 @@
     protected static final ServiceBundle MOCK_SERVICES =
             new ServiceBundle() {
                 @Override
+                public UiTopoLayoutService layout() {
+                    return MOCK_LAYOUT;
+                }
+
+                @Override
                 public ClusterService cluster() {
                     return MOCK_CLUSTER;
                 }
@@ -297,6 +348,7 @@
 
     private static final ClusterService MOCK_CLUSTER = new MockClusterService();
     private static final MastershipService MOCK_MASTER = new MockMasterService();
+    private static final UiTopoLayoutService MOCK_LAYOUT = new MockLayoutService();
     private static final RegionService MOCK_REGION = new MockRegionService();
     private static final DeviceService MOCK_DEVICE = new MockDeviceService();
     private static final LinkService MOCK_LINK = new MockLinkService();
@@ -384,6 +436,71 @@
         }
     }
 
+    // TODO: consider implementing UiTopoLayoutServiceAdapter and extending that here
+    private static class MockLayoutService implements UiTopoLayoutService {
+        private final Map<UiTopoLayoutId, UiTopoLayout> map = new HashMap<>();
+        private final Map<UiTopoLayoutId, Set<UiTopoLayout>> peers = new HashMap<>();
+        private final Map<RegionId, UiTopoLayout> byRegion = new HashMap<>();
+
+        MockLayoutService() {
+            map.put(LAYOUT_ROOT.id(), LAYOUT_ROOT);
+            map.put(LAYOUT_1.id(), LAYOUT_1);
+            map.put(LAYOUT_2.id(), LAYOUT_2);
+            map.put(LAYOUT_3.id(), LAYOUT_3);
+
+            peers.put(LAYOUT_ROOT.id(), ImmutableSet.of());
+            peers.put(LAYOUT_1.id(), ImmutableSet.of(LAYOUT_2, LAYOUT_3));
+            peers.put(LAYOUT_2.id(), ImmutableSet.of(LAYOUT_1, LAYOUT_3));
+            peers.put(LAYOUT_3.id(), ImmutableSet.of(LAYOUT_1, LAYOUT_2));
+
+            byRegion.put(REGION_1.id(), LAYOUT_1);
+            byRegion.put(REGION_2.id(), LAYOUT_2);
+            byRegion.put(REGION_3.id(), LAYOUT_3);
+        }
+
+        @Override
+        public UiTopoLayout getRootLayout() {
+            return LAYOUT_ROOT;
+        }
+
+        @Override
+        public Set<UiTopoLayout> getLayouts() {
+            return LAYOUT_SET;
+        }
+
+        @Override
+        public boolean addLayout(UiTopoLayout layout) {
+            return false;
+        }
+
+        @Override
+        public UiTopoLayout getLayout(UiTopoLayoutId layoutId) {
+            return map.get(layoutId);
+        }
+
+        @Override
+        public UiTopoLayout getLayout(RegionId regionId) {
+            return byRegion.get(regionId);
+        }
+
+        @Override
+        public Set<UiTopoLayout> getPeerLayouts(UiTopoLayoutId layoutId) {
+            return peers.get(layoutId);
+        }
+
+        @Override
+        public Set<UiTopoLayout> getChildren(UiTopoLayoutId layoutId) {
+            return LAYOUT_ROOT.id().equals(layoutId)
+                    ? ROOT_KIDS
+                    : Collections.emptySet();
+        }
+
+        @Override
+        public boolean removeLayout(UiTopoLayout layout) {
+            return false;
+        }
+    }
+
     // TODO: consider implementing RegionServiceAdapter and extending that here
     private static class MockRegionService implements RegionService {