[CORD-2721] Implement group bucket modification

Change-Id: I0f637ec4ff2b0c12db53d70fed195ea28e542535
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
index b9912ac..f72d11b 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -1568,38 +1568,71 @@
                                Sets.difference(new HashSet<>(prevIntf.ipAddressesList()),
                                                new HashSet<>(intf.ipAddressesList())));
 
-            if (prevIntf.vlanNative() != VlanId.NONE && !intf.vlanNative().equals(prevIntf.vlanNative())) {
-                // RemoveVlanNative
-                updateVlanConfigInternal(deviceId, portNum, prevIntf.vlanNative(), true, false);
+            if (!prevIntf.vlanNative().equals(VlanId.NONE)
+                    && !prevIntf.vlanNative().equals(intf.vlanUntagged())
+                    && !prevIntf.vlanNative().equals(intf.vlanNative())) {
+                if (intf.vlanTagged().contains(prevIntf.vlanNative())) {
+                    // Update filtering objective and L2IG group bucket
+                    updatePortVlanTreatment(deviceId, portNum, prevIntf.vlanNative(), false);
+                } else {
+                    // RemoveVlanNative
+                    updateVlanConfigInternal(deviceId, portNum, prevIntf.vlanNative(), true, false);
+                }
+            }
+
+            if (!prevIntf.vlanUntagged().equals(VlanId.NONE)
+                    && !prevIntf.vlanUntagged().equals(intf.vlanUntagged())
+                    && !prevIntf.vlanUntagged().equals(intf.vlanNative())) {
+                if (intf.vlanTagged().contains(prevIntf.vlanUntagged())) {
+                    // Update filtering objective and L2IG group bucket
+                    updatePortVlanTreatment(deviceId, portNum, prevIntf.vlanUntagged(), false);
+                } else {
+                    // RemoveVlanUntagged
+                    updateVlanConfigInternal(deviceId, portNum, prevIntf.vlanUntagged(), true, false);
+                }
             }
 
             if (!prevIntf.vlanTagged().isEmpty() && !intf.vlanTagged().equals(prevIntf.vlanTagged())) {
                 // RemoveVlanTagged
-                prevIntf.vlanTagged().stream().filter(i -> !intf.vlanTagged().contains(i)).forEach(
-                        vlanId -> updateVlanConfigInternal(deviceId, portNum, vlanId, false, false)
-                );
+                Sets.difference(prevIntf.vlanTagged(), intf.vlanTagged()).stream()
+                        .filter(i -> !intf.vlanUntagged().equals(i))
+                        .filter(i -> !intf.vlanNative().equals(i))
+                        .forEach(vlanId -> updateVlanConfigInternal(
+                                deviceId, portNum, vlanId, false, false));
             }
 
-            if (prevIntf.vlanUntagged() != VlanId.NONE && !intf.vlanUntagged().equals(prevIntf.vlanUntagged())) {
-                // RemoveVlanUntagged
-                updateVlanConfigInternal(deviceId, portNum, prevIntf.vlanUntagged(), true, false);
-            }
-
-            if (intf.vlanNative() != VlanId.NONE && !prevIntf.vlanNative().equals(intf.vlanNative())) {
-                // AddVlanNative
-                updateVlanConfigInternal(deviceId, portNum, intf.vlanNative(), true, true);
+            if (!intf.vlanNative().equals(VlanId.NONE)
+                    && !prevIntf.vlanNative().equals(intf.vlanNative())
+                    && !prevIntf.vlanUntagged().equals(intf.vlanNative())) {
+                if (prevIntf.vlanTagged().contains(intf.vlanNative())) {
+                    // Update filtering objective and L2IG group bucket
+                    updatePortVlanTreatment(deviceId, portNum, intf.vlanNative(), true);
+                } else {
+                    // AddVlanNative
+                    updateVlanConfigInternal(deviceId, portNum, intf.vlanNative(), true, true);
+                }
             }
 
             if (!intf.vlanTagged().isEmpty() && !intf.vlanTagged().equals(prevIntf.vlanTagged())) {
                 // AddVlanTagged
-                intf.vlanTagged().stream().filter(i -> !prevIntf.vlanTagged().contains(i)).forEach(
-                        vlanId -> updateVlanConfigInternal(deviceId, portNum, vlanId, false, true)
+                Sets.difference(intf.vlanTagged(), prevIntf.vlanTagged()).stream()
+                        .filter(i -> !prevIntf.vlanUntagged().equals(i))
+                        .filter(i -> !prevIntf.vlanNative().equals(i))
+                        .forEach(vlanId -> updateVlanConfigInternal(
+                                deviceId, portNum, vlanId, false, true)
                 );
             }
 
-            if (intf.vlanUntagged() != VlanId.NONE && !prevIntf.vlanUntagged().equals(intf.vlanUntagged())) {
-                // AddVlanUntagged
-                updateVlanConfigInternal(deviceId, portNum, intf.vlanUntagged(), true, true);
+            if (!intf.vlanUntagged().equals(VlanId.NONE)
+                    && !prevIntf.vlanUntagged().equals(intf.vlanUntagged())
+                    && !prevIntf.vlanNative().equals(intf.vlanUntagged())) {
+                if (prevIntf.vlanTagged().contains(intf.vlanUntagged())) {
+                    // Update filtering objective and L2IG group bucket
+                    updatePortVlanTreatment(deviceId, portNum, intf.vlanUntagged(), true);
+                } else {
+                    // AddVlanUntagged
+                    updateVlanConfigInternal(deviceId, portNum, intf.vlanUntagged(), true, true);
+                }
             }
             addSubnetConfig(prevIntf.connectPoint(),
                             Sets.difference(new HashSet<>(intf.ipAddressesList()),
@@ -1609,6 +1642,26 @@
         }
     }
 
+    private void updatePortVlanTreatment(DeviceId deviceId, PortNumber portNum,
+                                         VlanId vlanId, boolean pushVlan) {
+        DefaultGroupHandler grpHandler = getGroupHandler(deviceId);
+        if (grpHandler == null) {
+            log.warn("Failed to retrieve group handler for device {}", deviceId);
+            return;
+        }
+
+        // Update filtering objective for a single port
+        routingRulePopulator.updateSinglePortFilters(deviceId, portNum, !pushVlan, vlanId, false);
+        routingRulePopulator.updateSinglePortFilters(deviceId, portNum, pushVlan, vlanId, true);
+
+        if (getVlanNextObjectiveId(deviceId, vlanId) != -1) {
+            // Update L2IG bucket of the port
+            grpHandler.updateL2InterfaceGroupBucket(portNum, vlanId, pushVlan);
+        } else {
+            log.warn("Failed to retrieve next objective for vlan {} in device {}:{}", vlanId, deviceId, portNum);
+        }
+    }
+
     private void updateVlanConfigInternal(DeviceId deviceId, PortNumber portNum,
                                           VlanId vlanId, boolean pushVlan, boolean install) {
         DefaultGroupHandler grpHandler = getGroupHandler(deviceId);
@@ -1628,7 +1681,7 @@
         if (nextId != -1 && !install) {
             // Update next objective for a single port as an output port
             // Remove a single port from L2FG
-            grpHandler.updateGroupFromVlanConfiguration(portNum, Collections.singleton(vlanId), nextId, install);
+            grpHandler.updateGroupFromVlanConfiguration(vlanId, portNum, nextId, install);
             // Remove L2 Bridging rule and L3 Unicast rule to the host
             hostHandler.processIntfVlanUpdatedEvent(deviceId, portNum, vlanId, pushVlan, install);
             // Remove broadcast forwarding rule and corresponding L2FG for VLAN
@@ -1645,7 +1698,7 @@
         } else if (install) {
             if (nextId != -1) {
                 // Add a single port to L2FG
-                grpHandler.updateGroupFromVlanConfiguration(portNum, Collections.singleton(vlanId), nextId, install);
+                grpHandler.updateGroupFromVlanConfiguration(vlanId, portNum, nextId, install);
             } else {
                 // Create L2FG for VLAN
                 grpHandler.createBcastGroupFromVlan(vlanId, Collections.singleton(portNum));
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java
index 75cab32..3cf1331 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java
@@ -1277,12 +1277,56 @@
         bc.run();
     }
 
-    public void updateGroupFromVlanConfiguration(PortNumber portNumber, Collection<VlanId> vlanIds,
-                                                 int nextId, boolean install) {
-        vlanIds.forEach(vlanId -> updateGroupFromVlanInternal(vlanId, portNumber, nextId, install));
+    /**
+     * Modifies L2IG bucket when the interface configuration is updated, especially
+     * when the interface has same VLAN ID but the VLAN type is changed (e.g., from
+     * vlan-tagged [10] to vlan-untagged 10), which requires changes on
+     * TrafficTreatment in turn.
+     *
+     * @param portNumber the port on this device that needs to be updated
+     * @param vlanId the vlan id corresponding to this port
+     * @param pushVlan indicates if packets should be sent out untagged or not out
+     *                 from the port. If true, updated TrafficTreatment involves
+     *                 pop vlan tag action. If false, updated TrafficTreatment
+     *                 does not involve pop vlan tag action.
+     */
+    public void updateL2InterfaceGroupBucket(PortNumber portNumber, VlanId vlanId, boolean pushVlan) {
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+        if (pushVlan) {
+            tBuilder.popVlan();
+        }
+        tBuilder.setOutput(portNumber);
+
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+        int nextId = getVlanNextObjectiveId(vlanId);
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.SIMPLE).fromApp(appId)
+                .addTreatment(tBuilder.build())
+                .withMeta(metadata);
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("port {} successfully updated NextObj {} on {}",
+                                         portNumber, nextId, deviceId),
+                (objective, error) ->
+                        log.warn("port {} failed to updated NextObj {} on {}: {}",
+                                 portNumber, nextId, deviceId, error));
+
+        flowObjectiveService.next(deviceId, nextObjBuilder.modify(context));
     }
 
-    private void updateGroupFromVlanInternal(VlanId vlanId, PortNumber portNum, int nextId, boolean install) {
+    /**
+     * Adds a single port to the L2FG or removes it from the L2FG.
+     *
+     * @param vlanId the vlan id corresponding to this port
+     * @param portNum the port on this device to be updated
+     * @param nextId the next objective ID for the given vlan id
+     * @param install if true, adds the port to L2FG. If false, removes it from L2FG.
+     */
+    public void updateGroupFromVlanConfiguration(VlanId vlanId, PortNumber portNum, int nextId, boolean install) {
         TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
         if (toPopVlan(portNum, vlanId)) {
             tBuilder.popVlan();
diff --git a/core/api/src/main/java/org/onosproject/net/flowobjective/DefaultNextObjective.java b/core/api/src/main/java/org/onosproject/net/flowobjective/DefaultNextObjective.java
index 671cc5f..8c466c9 100644
--- a/core/api/src/main/java/org/onosproject/net/flowobjective/DefaultNextObjective.java
+++ b/core/api/src/main/java/org/onosproject/net/flowobjective/DefaultNextObjective.java
@@ -316,6 +316,24 @@
         }
 
         @Override
+        public NextObjective modify() {
+            return modify(null);
+        }
+
+        @Override
+        public NextObjective modify(ObjectiveContext context) {
+            treatments = listBuilder.build();
+            op = Operation.MODIFY;
+            this.context = context;
+            checkNotNull(appId, "Must supply an application id");
+            checkNotNull(id, "id cannot be null");
+            checkNotNull(type, "The type cannot be null");
+            checkArgument(!treatments.isEmpty(), "Must have at least one treatment");
+
+            return new DefaultNextObjective(this);
+        }
+
+        @Override
         public NextObjective verify() {
             return verify(null);
         }
diff --git a/core/api/src/main/java/org/onosproject/net/flowobjective/NextObjective.java b/core/api/src/main/java/org/onosproject/net/flowobjective/NextObjective.java
index 725ebf5..4cc8478 100644
--- a/core/api/src/main/java/org/onosproject/net/flowobjective/NextObjective.java
+++ b/core/api/src/main/java/org/onosproject/net/flowobjective/NextObjective.java
@@ -228,6 +228,23 @@
         NextObjective removeFromExisting(ObjectiveContext context);
 
         /**
+         * Build the next objective that will be modified with {@link Operation}
+         * MODIFY.
+         *
+         * @return a next objective
+         */
+
+        NextObjective modify();
+        /**
+         * Build the next objective that will be modified, with {@link Operation}
+         * MODIFY. The context will be used to notify the calling application.
+         *
+         * @param context an objective context
+         * @return a next objective
+         */
+        NextObjective modify(ObjectiveContext context);
+
+        /**
          * Builds the next objective that needs to be verified.
          *
          * @return a next objective with {@link Operation} VERIFY
diff --git a/core/api/src/main/java/org/onosproject/net/flowobjective/Objective.java b/core/api/src/main/java/org/onosproject/net/flowobjective/Objective.java
index f0e4305..3b0346c 100644
--- a/core/api/src/main/java/org/onosproject/net/flowobjective/Objective.java
+++ b/core/api/src/main/java/org/onosproject/net/flowobjective/Objective.java
@@ -63,6 +63,11 @@
         REMOVE_FROM_EXISTING,
 
         /**
+         * Modify an existing Next Objective. Can be used to modify group buckets.
+         */
+        MODIFY,
+
+        /**
          * Verifies that an existing Next Objective's collection of treatments
          * are correctly represented by the underlying implementation of the objective.
          * Corrective action is taken if discrepancies are found during verification.
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2GroupHandler.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2GroupHandler.java
index e41ca4c..fdee8d6 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2GroupHandler.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2GroupHandler.java
@@ -804,24 +804,6 @@
         });
     }
 
-    private List<GroupBucket> createL3MulticastBucket(List<GroupInfo> groupInfos) {
-        List<GroupBucket> l3McastBuckets = new ArrayList<>();
-        // For each inner group
-        groupInfos.forEach(groupInfo -> {
-            // Points to L3 interface group if there is one.
-            // Otherwise points to L2 interface group directly.
-            GroupDescription nextGroupDesc = (groupInfo.nextGroupDesc() != null) ?
-                    groupInfo.nextGroupDesc() : groupInfo.innerMostGroupDesc();
-            TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
-            ttb.group(new GroupId(nextGroupDesc.givenGroupId()));
-            GroupBucket abucket = DefaultGroupBucket.createAllGroupBucket(ttb.build());
-            l3McastBuckets.add(abucket);
-        });
-        // Done return the new list of buckets
-        return l3McastBuckets;
-    }
-
-
     private void createL3MulticastGroup(NextObjective nextObj, VlanId vlanId,
                                         List<GroupInfo> groupInfos) {
         // Let's create a new list mcast buckets
@@ -1197,7 +1179,8 @@
         newBuckets = generateNextGroupBuckets(unsentGroups, SELECT);
 
         // retrieve the original L3 ECMP group
-        Group l3ecmpGroup = retrieveTopLevelGroup(allActiveKeys, nextObjective.id());
+        Group l3ecmpGroup = retrieveTopLevelGroup(allActiveKeys, deviceId,
+                                                  groupService, nextObjective.id());
         if (l3ecmpGroup == null) {
             fail(nextObjective, ObjectiveError.GROUPMISSING);
             return;
@@ -1270,7 +1253,8 @@
                                          List<Deque<GroupKey>> allActiveKeys,
                                          List<GroupInfo> groupInfos,
                                          VlanId assignedVlan) {
-        Group l2FloodGroup = retrieveTopLevelGroup(allActiveKeys, nextObj.id());
+        Group l2FloodGroup = retrieveTopLevelGroup(allActiveKeys, deviceId,
+                                                   groupService, nextObj.id());
 
         if (l2FloodGroup == null) {
             log.warn("Can't find L2 flood group while adding bucket to it. NextObj = {}",
@@ -1350,7 +1334,8 @@
         List<GroupBucket> newBuckets = createL3MulticastBucket(groupInfos);
 
         // get the group being edited
-        Group l3mcastGroup = retrieveTopLevelGroup(allActiveKeys, nextObj.id());
+        Group l3mcastGroup = retrieveTopLevelGroup(allActiveKeys, deviceId,
+                                                   groupService, nextObj.id());
         if (l3mcastGroup == null) {
             fail(nextObj, ObjectiveError.GROUPMISSING);
             return;
@@ -1579,6 +1564,56 @@
     }
 
     /**
+     * Modify buckets in the L2 interface group.
+     *
+     * @param nextObjective a next objective that contains information for the
+     *                      buckets to be modified in the group
+     * @param next the representation of the existing group-chains for this next
+     *             objective, from which the innermost group buckets to remove are determined
+     */
+    protected void modifyBucketFromGroup(NextObjective nextObjective, NextGroup next) {
+        if (nextObjective.type() != NextObjective.Type.SIMPLE) {
+            log.warn("ModifyBucketFromGroup cannot be applied to nextType:{} in dev:{} for next:{}",
+                     nextObjective.type(), deviceId, nextObjective.id());
+            fail(nextObjective, ObjectiveError.UNSUPPORTED);
+            return;
+        }
+
+        VlanId assignedVlan = readVlanFromSelector(nextObjective.meta());
+        if (assignedVlan == null) {
+            log.warn("VLAN ID required by simple next obj is missing. Abort.");
+            fail(nextObjective, ObjectiveError.BADPARAMS);
+            return;
+        }
+
+        List<GroupInfo> groupInfos = prepareL2InterfaceGroup(nextObjective, assignedVlan);
+
+        // There is only one L2 interface group in this case
+        GroupDescription l2InterfaceGroupDesc = groupInfos.get(0).innerMostGroupDesc();
+
+        // Replace group bucket for L2 interface group
+        groupService.setBucketsForGroup(deviceId,
+                                        l2InterfaceGroupDesc.appCookie(),
+                                        l2InterfaceGroupDesc.buckets(),
+                                        l2InterfaceGroupDesc.appCookie(),
+                                        l2InterfaceGroupDesc.appId());
+
+        // update store - synchronize access as there may be multiple threads
+        // trying to remove buckets from the same group, each with its own
+        // potentially stale copy of allActiveKeys
+        synchronized (flowObjectiveStore) {
+            List<Deque<GroupKey>> modifiedGroupKeys = Lists.newArrayList();
+            ArrayDeque<GroupKey> top = new ArrayDeque<>();
+            top.add(l2InterfaceGroupDesc.appCookie());
+            modifiedGroupKeys.add(top);
+
+            flowObjectiveStore.putNextGroup(nextObjective.id(),
+                                            new OfdpaNextGroup(modifiedGroupKeys,
+                                                               nextObjective));
+        }
+    }
+
+    /**
      *  Checks existing buckets in {@link NextGroup}  to verify if they match
      *  the buckets in the given {@link NextObjective}. Adds or removes buckets
      *  to ensure that the buckets match up.
@@ -1840,24 +1875,6 @@
         return (int) nextIndex.incrementAndGet();
     }
 
-    protected Group retrieveTopLevelGroup(List<Deque<GroupKey>> allActiveKeys,
-                                          int nextid) {
-        GroupKey topLevelGroupKey;
-        if (!allActiveKeys.isEmpty()) {
-            topLevelGroupKey = allActiveKeys.get(0).peekFirst();
-        } else {
-            log.warn("Could not determine top level group while processing"
-                             + "next:{} in dev:{}", nextid, deviceId);
-            return null;
-        }
-        Group topGroup = groupService.getGroup(deviceId, topLevelGroupKey);
-        if (topGroup == null) {
-            log.warn("Could not find top level group while processing "
-                             + "next:{} in dev:{}", nextid, deviceId);
-        }
-        return topGroup;
-    }
-
     protected void processPendingAddGroupsOrNextObjs(GroupKey key, boolean added) {
         //first check for group chain
         Set<OfdpaGroupHandlerUtility.GroupChainElem> gceSet = pendingGroups.asMap().remove(key);
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2Pipeline.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2Pipeline.java
index 661d3e9..b481c3f 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2Pipeline.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/Ofdpa2Pipeline.java
@@ -381,6 +381,16 @@
                       nextObjective.id(), deviceId);
             groupHandler.removeBucketFromGroup(nextObjective, nextGroup);
             break;
+        case MODIFY:
+            if (nextGroup == null) {
+                log.warn("Cannot modify next {} that does not exist in device {}",
+                         nextObjective.id(), deviceId);
+                return;
+            }
+            log.debug("Processing NextObjective id {} in dev {} - modify bucket",
+                      nextObjective.id(), deviceId);
+            groupHandler.modifyBucketFromGroup(nextObjective, nextGroup);
+            break;
         case VERIFY:
             if (nextGroup == null) {
                 log.warn("Cannot verify next {} that does not exist in device {}",
diff --git a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
index e9c97bc..21802ea 100644
--- a/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
+++ b/drivers/default/src/main/java/org/onosproject/driver/pipeline/ofdpa/OfdpaGroupHandlerUtility.java
@@ -394,6 +394,43 @@
         return ImmutableList.copyOf(newBuckets);
     }
 
+    static List<GroupBucket> createL3MulticastBucket(List<GroupInfo> groupInfos) {
+        List<GroupBucket> l3McastBuckets = new ArrayList<>();
+        // For each inner group
+        groupInfos.forEach(groupInfo -> {
+            // Points to L3 interface group if there is one.
+            // Otherwise points to L2 interface group directly.
+            GroupDescription nextGroupDesc = (groupInfo.nextGroupDesc() != null) ?
+                    groupInfo.nextGroupDesc() : groupInfo.innerMostGroupDesc();
+            TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
+            ttb.group(new GroupId(nextGroupDesc.givenGroupId()));
+            GroupBucket abucket = DefaultGroupBucket.createAllGroupBucket(ttb.build());
+            l3McastBuckets.add(abucket);
+        });
+        // Done return the new list of buckets
+        return l3McastBuckets;
+    }
+
+    static Group retrieveTopLevelGroup(List<Deque<GroupKey>> allActiveKeys,
+                                       DeviceId deviceId,
+                                       GroupService groupService,
+                                       int nextid) {
+        GroupKey topLevelGroupKey;
+        if (!allActiveKeys.isEmpty()) {
+            topLevelGroupKey = allActiveKeys.get(0).peekFirst();
+        } else {
+            log.warn("Could not determine top level group while processing"
+                             + "next:{} in dev:{}", nextid, deviceId);
+            return null;
+        }
+        Group topGroup = groupService.getGroup(deviceId, topLevelGroupKey);
+        if (topGroup == null) {
+            log.warn("Could not find top level group while processing "
+                             + "next:{} in dev:{}", nextid, deviceId);
+        }
+        return topGroup;
+    }
+
     /**
      * Extracts VlanId from given group ID.
      *