Per container flow statistics filtering to account cFlows
[controller.git] / opendaylight / protocol_plugins / openflow / src / main / java / org / opendaylight / controller / protocol_plugin / openflow / internal / ReadServiceFilter.java
index 1b71c3bec34f70b382af55c094e7acb7f86f0fdf..2c8708f20ee46295972a6b18b280df436f009dfc 100644 (file)
 package org.opendaylight.controller.protocol_plugin.openflow.internal;
 
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 
+import org.opendaylight.controller.protocol_plugin.openflow.IOFStatisticsListener;
 import org.opendaylight.controller.protocol_plugin.openflow.IOFStatisticsManager;
-import org.opendaylight.controller.protocol_plugin.openflow.IPluginReadServiceFilter;
+import org.opendaylight.controller.protocol_plugin.openflow.IReadFilterInternalListener;
+import org.opendaylight.controller.protocol_plugin.openflow.IReadServiceFilter;
 import org.opendaylight.controller.protocol_plugin.openflow.core.IController;
-import org.openflow.protocol.OFMatch;
-import org.openflow.protocol.statistics.OFPortStatisticsReply;
-import org.openflow.protocol.statistics.OFStatistics;
-import org.openflow.protocol.statistics.OFStatisticsType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.opendaylight.controller.sal.action.Action;
 import org.opendaylight.controller.sal.action.ActionType;
 import org.opendaylight.controller.sal.action.Output;
@@ -33,6 +30,7 @@ import org.opendaylight.controller.sal.core.ContainerFlow;
 import org.opendaylight.controller.sal.core.IContainerListener;
 import org.opendaylight.controller.sal.core.Node;
 import org.opendaylight.controller.sal.core.NodeConnector;
+import org.opendaylight.controller.sal.core.NodeTable;
 import org.opendaylight.controller.sal.core.UpdateType;
 import org.opendaylight.controller.sal.flowprogrammer.Flow;
 import org.opendaylight.controller.sal.match.Match;
@@ -40,24 +38,33 @@ import org.opendaylight.controller.sal.match.MatchType;
 import org.opendaylight.controller.sal.reader.FlowOnNode;
 import org.opendaylight.controller.sal.reader.NodeConnectorStatistics;
 import org.opendaylight.controller.sal.reader.NodeDescription;
+import org.opendaylight.controller.sal.reader.NodeTableStatistics;
 import org.opendaylight.controller.sal.utils.GlobalConstants;
 import org.opendaylight.controller.sal.utils.NodeConnectorCreator;
 import org.opendaylight.controller.sal.utils.NodeCreator;
-
+import org.opendaylight.controller.sal.utils.NodeTableCreator;
+import org.openflow.protocol.OFMatch;
+import org.openflow.protocol.statistics.OFFlowStatisticsReply;
+import org.openflow.protocol.statistics.OFPortStatisticsReply;
+import org.openflow.protocol.statistics.OFStatistics;
+import org.openflow.protocol.statistics.OFStatisticsType;
+import org.openflow.protocol.statistics.OFTableStatistics;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 /**
  * Read Service shim layer which is in charge of filtering the flow statistics
  * based on container. It is a Global instance.
- *
- *
- *
  */
-public class ReadServiceFilter implements IPluginReadServiceFilter,
-        IContainerListener {
+public class ReadServiceFilter implements IReadServiceFilter, IContainerListener, IOFStatisticsListener {
     private static final Logger logger = LoggerFactory
             .getLogger(ReadServiceFilter.class);
     private IController controller = null;
     private IOFStatisticsManager statsMgr = null;
-    private Map<String, Set<NodeConnector>> containerToNc;
+    private ConcurrentMap<String, Set<NodeConnector>> containerToNc;
+    private ConcurrentMap<String, Set<Node>> containerToNode;
+    private ConcurrentMap<String, Set<NodeTable>> containerToNt;
+    private ConcurrentMap<String, Set<ContainerFlow>> containerFlows;
+    private ConcurrentMap<String, IReadFilterInternalListener> readFilterInternalListeners;
 
     public void setController(IController core) {
         this.controller = core;
@@ -69,13 +76,50 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
         }
     }
 
+    public void setReadFilterInternalListener(Map<?, ?> props, IReadFilterInternalListener s) {
+        if (props == null) {
+            logger.error("Failed setting Read Filter Listener, property map is null.");
+            return;
+        }
+        String containerName = (String) props.get("containerName");
+        if (containerName == null) {
+            logger.error("Failed setting Read Filter Listener, container name not supplied.");
+            return;
+        }
+        if ((this.readFilterInternalListeners != null) && !this.readFilterInternalListeners.containsValue(s)) {
+            this.readFilterInternalListeners.put(containerName, s);
+            logger.trace("Added Read Filter Listener for container {}", containerName);
+        }
+    }
+
+    public void unsetReadFilterInternalListener(Map<?, ?> props, IReadFilterInternalListener s) {
+        if (props == null) {
+            logger.error("Failed unsetting Read Filter Listener, property map is null.");
+            return;
+        }
+        String containerName = (String) props.get("containerName");
+        if (containerName == null) {
+            logger.error("Failed unsetting Read Filter Listener, containerName not supplied");
+            return;
+        }
+        if ((this.readFilterInternalListeners != null) && this.readFilterInternalListeners.get(containerName) != null
+                && this.readFilterInternalListeners.get(containerName).equals(s)) {
+            this.readFilterInternalListeners.remove(containerName);
+            logger.trace("Removed Read Filter Listener for container {}", containerName);
+        }
+    }
+
     /**
      * Function called by the dependency manager when all the required
      * dependencies are satisfied
      *
      */
     void init() {
-        containerToNc = new HashMap<String, Set<NodeConnector>>();
+        containerToNc = new ConcurrentHashMap<String, Set<NodeConnector>>();
+        containerToNt = new ConcurrentHashMap<String, Set<NodeTable>>();
+        containerToNode = new ConcurrentHashMap<String, Set<Node>>();
+        containerFlows = new ConcurrentHashMap<String, Set<ContainerFlow>>();
+        readFilterInternalListeners = new ConcurrentHashMap<String, IReadFilterInternalListener>();
     }
 
     /**
@@ -114,8 +158,7 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
     }
 
     @Override
-    public FlowOnNode readFlow(String container, Node node, Flow flow,
-            boolean cached) {
+    public FlowOnNode readFlow(String container, Node node, Flow flow, boolean cached) {
 
         if (controller == null) {
             // Avoid to provide cached statistics if controller went down.
@@ -126,20 +169,25 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
 
         long sid = (Long) node.getID();
         OFMatch ofMatch = new FlowConverter(flow).getOFMatch();
-        List<OFStatistics> ofList = (cached == true) ? statsMgr
-                .getOFFlowStatistics(sid, ofMatch) : statsMgr.queryStatistics(
-                sid, OFStatisticsType.FLOW, ofMatch);
+        List<OFStatistics> ofList;
+        if (cached == true){
+            ofList = statsMgr.getOFFlowStatistics(sid, ofMatch, flow.getPriority());
+        } else {
+            ofList = statsMgr.queryStatistics(sid, OFStatisticsType.FLOW, ofMatch);
+            for (OFStatistics ofStat : ofList) {
+                if (((OFFlowStatisticsReply)ofStat).getPriority() == flow.getPriority()){
+                    ofList = new ArrayList<OFStatistics>(1);
+                    ofList.add(ofStat);
+                    break;
+                }
+            }
+        }
 
-        /*
-         * Convert and filter the statistics per container
-         */
-        List<FlowOnNode> flowOnNodeList = new FlowStatisticsConverter(ofList)
-                .getFlowOnNodeList(node);
-        List<FlowOnNode> filteredList = filterFlowListPerContainer(container,
-                node, flowOnNodeList);
+        // Convert and filter the statistics per container
+        List<FlowOnNode> flowOnNodeList = new FlowStatisticsConverter(ofList).getFlowOnNodeList(node);
+        List<FlowOnNode> filteredList = filterFlowListPerContainer(container, node, flowOnNodeList);
 
-        return (filteredList == null || filteredList.isEmpty()) ? null
-                : filteredList.get(0);
+        return (filteredList == null || filteredList.isEmpty()) ? null : filteredList.get(0);
     }
 
     @Override
@@ -151,13 +199,9 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
                 .getOFFlowStatistics(sid) : statsMgr.queryStatistics(sid,
                 OFStatisticsType.FLOW, null);
 
-        /*
-         * Convert and filter the statistics per container
-         */
-        List<FlowOnNode> flowOnNodeList = new FlowStatisticsConverter(ofList)
-                .getFlowOnNodeList(node);
-        List<FlowOnNode> filteredList = filterFlowListPerContainer(container,
-                node, flowOnNodeList);
+        // Convert and filter the statistics per container
+        List<FlowOnNode> flowOnNodeList = new FlowStatisticsConverter(ofList).getFlowOnNodeList(node);
+        List<FlowOnNode> filteredList = filterFlowListPerContainer(container, node, flowOnNodeList);
 
         return (filteredList == null) ? null : filteredList;
 
@@ -174,7 +218,7 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
         long sid = (Long) node.getID();
         List<OFStatistics> ofList = (cached == true) ? statsMgr
                 .getOFDescStatistics(sid) : statsMgr.queryStatistics(sid,
-                OFStatisticsType.DESC, null);
+                        OFStatisticsType.DESC, null);
 
         return new DescStatisticsConverter(ofList).getHwDescription();
     }
@@ -207,15 +251,14 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
     }
 
     /**
-     * Filters a list of FlowOnNode elements based on the container
+     * Filters a list of OFStatistics elements based on the container
      *
      * @param container
      * @param nodeId
      * @param list
      * @return
      */
-    public List<OFStatistics> filterPortListPerContainer(String container,
-            long switchId, List<OFStatistics> list) {
+    public List<OFStatistics> filterPortListPerContainer(String container, long switchId, List<OFStatistics> list) {
         if (list == null) {
             return null;
         }
@@ -235,6 +278,27 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
         return newList;
     }
 
+
+    public List<OFStatistics> filterTableListPerContainer(
+            String container, long switchId, List<OFStatistics> list) {
+        if (list == null) {
+            return null;
+        }
+
+        // Create new filtered list of node tables
+        List<OFStatistics> newList = new ArrayList<OFStatistics>();
+
+        for (OFStatistics stat : list) {
+            OFTableStatistics target = (OFTableStatistics) stat;
+            NodeTable nt = NodeTableCreator.createOFNodeTable(target.getTableId(), NodeCreator.createOFNode(switchId));
+            if (containerOwnsNodeTable(container, nt)) {
+                newList.add(target);
+            }
+        }
+
+        return newList;
+    }
+
     /**
      * Returns whether the specified flow (flow match + actions)
      * belongs to the container
@@ -249,17 +313,17 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
         if (container.equals(GlobalConstants.DEFAULT.toString())) {
             return true;
         }
-        return (flowPortsBelongToContainer(container, node, flow)
-                && flowVlanBelongsToContainer(container, node, flow) && flowSpecAllowsFlow(
-                container, flow.getMatch()));
+        return (flowPortsBelongToContainer(container, node, flow) &&
+                flowVlanBelongsToContainer(container, node, flow) &&
+                isFlowAllowedByContainer(container, flow));
     }
 
     /**
      * Returns whether the passed NodeConnector belongs to the container
      *
-     * @param container        container name
-     * @param p                node connector to test
-     * @return                 true if belongs false otherwise
+     * @param container container name
+     * @param p     node connector to test
+     * @return          true if belongs false otherwise
      */
     public boolean containerOwnsNodeConnector(String container, NodeConnector p) {
         // All node connectors belong to the default container
@@ -271,14 +335,39 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
     }
 
     /**
-     * Returns whether the container flowspec allows the passed flow
+     * Returns whether the passed NodeConnector belongs to the container
+     *
+     * @param container container name
+     * @param table     node table to test
+     * @return          true if belongs false otherwise
+     */
+    public boolean containerOwnsNodeTable(String container, NodeTable table) {
+        // All node table belong to the default container
+        if (container.equals(GlobalConstants.DEFAULT.toString())) {
+            return true;
+        }
+        Set<NodeTable> tableSet = containerToNt.get(container);
+        return (tableSet == null) ? false : tableSet.contains(table);
+    }
+
+    /**
+     * Returns whether the container flows allow the passed flow
      *
      * @param container
      * @param match
      * @return
      */
-    private boolean flowSpecAllowsFlow(String container, Match match) {
-        return true; // Always true for now
+    private boolean isFlowAllowedByContainer(String container, Flow flow) {
+        Set<ContainerFlow> cFlowSet = this.containerFlows.get(container);
+        if (cFlowSet == null || cFlowSet.isEmpty()) {
+            return true;
+        }
+        for (ContainerFlow cFlow : cFlowSet) {
+            if (cFlow.allowsFlow(flow)) {
+                return true;
+            }
+        }
+        return false;
     }
 
     /**
@@ -290,8 +379,7 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
      * @param flow
      * @return
      */
-    private boolean flowVlanBelongsToContainer(String container, Node node,
-            Flow flow) {
+    private boolean flowVlanBelongsToContainer(String container, Node node, Flow flow) {
         return true; // Always true for now
     }
 
@@ -308,9 +396,7 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
             Flow flow) {
         Match m = flow.getMatch();
         if (m.isPresent(MatchType.IN_PORT)) {
-            NodeConnector inPort = (NodeConnector) m
-                    .getField(MatchType.IN_PORT).getValue();
-
+            NodeConnector inPort = (NodeConnector) m.getField(MatchType.IN_PORT).getValue();
             // If the incoming port is specified, check if it belongs to
             if (!containerOwnsNodeConnector(container, inPort)) {
                 return false;
@@ -320,8 +406,7 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
         // If an outgoing port is specified, it must belong to this container
         for (Action action : flow.getActions()) {
             if (action.getType() == ActionType.OUTPUT) {
-                NodeConnector outPort = (NodeConnector) ((Output) action)
-                        .getPort();
+                NodeConnector outPort = ((Output) action).getPort();
                 if (!containerOwnsNodeConnector(container, outPort)) {
                     return false;
                 }
@@ -331,49 +416,84 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
     }
 
     @Override
-    public void containerFlowUpdated(String containerName,
-            ContainerFlow previousFlow, ContainerFlow currentFlow, UpdateType t) {
-
+    public void containerFlowUpdated(String containerName, ContainerFlow previousFlow,
+            ContainerFlow currentFlow, UpdateType t) {
+        Set<ContainerFlow> cFlowSet = containerFlows.get(containerName);
+        switch (t) {
+        case ADDED:
+            if (cFlowSet == null) {
+                cFlowSet = new HashSet<ContainerFlow>();
+                containerFlows.put(containerName, cFlowSet);
+            }
+            cFlowSet.add(currentFlow);
+        case CHANGED:
+            break;
+        case REMOVED:
+            if (cFlowSet != null) {
+                cFlowSet.remove(currentFlow);
+            }
+            break;
+        default:
+            break;
+        }
     }
 
     @Override
-    public void nodeConnectorUpdated(String containerName, NodeConnector p,
-            UpdateType type) {
-        Set<NodeConnector> target = null;
+    public void nodeConnectorUpdated(String containerName, NodeConnector p, UpdateType type) {
 
         switch (type) {
         case ADDED:
             if (!containerToNc.containsKey(containerName)) {
-                containerToNc.put(containerName, new HashSet<NodeConnector>());
+                containerToNc.put(containerName,
+                    Collections.newSetFromMap(new ConcurrentHashMap<NodeConnector,Boolean>()));
             }
             containerToNc.get(containerName).add(p);
-            break;
-        case CHANGED:
+            if (!containerToNode.containsKey(containerName)) {
+                containerToNode.put(containerName, new HashSet<Node>());
+            }
+            containerToNode.get(containerName).add(p.getNode());
             break;
         case REMOVED:
-            target = containerToNc.get(containerName);
-            if (target != null) {
-                target.remove(p);
+            Set<NodeConnector> ncSet = containerToNc.get(containerName);
+            if (ncSet != null) {
+                //remove this nc from container map
+                ncSet.remove(p);
+
+                //check if there are still ports of this node in this container
+                //and if not, remove its mapping
+                boolean nodeInContainer = false;
+                Node node = p.getNode();
+                for (NodeConnector nodeConnector : ncSet) {
+                    if (nodeConnector.getNode().equals(node)){
+                        nodeInContainer = true;
+                        break;
+                    }
+                }
+                if (! nodeInContainer) {
+                    Set<Node> nodeSet = containerToNode.get(containerName);
+                    if (nodeSet != null) {
+                        nodeSet.remove(node);
+                    }
+                }
             }
             break;
+        case CHANGED:
         default:
         }
     }
 
     @Override
-    public void tagUpdated(String containerName, Node n, short oldTag,
-            short newTag, UpdateType t) {
+    public void tagUpdated(String containerName, Node n, short oldTag, short newTag, UpdateType t) {
         // Not interested in this event
     }
 
     @Override
     public void containerModeUpdated(UpdateType t) {
-        // do nothing
+        // Not interested in this event
     }
 
     @Override
-    public NodeConnectorStatistics readNodeConnector(String containerName,
-            NodeConnector connector, boolean cached) {
+    public NodeConnectorStatistics readNodeConnector(String containerName, NodeConnector connector, boolean cached) {
         if (!containerOwnsNodeConnector(containerName, connector)) {
             return null;
         }
@@ -382,28 +502,24 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
         short portId = (Short) connector.getID();
         List<OFStatistics> ofList = (cached == true) ? statsMgr
                 .getOFPortStatistics(sid, portId) : statsMgr.queryStatistics(
-                sid, OFStatisticsType.PORT, portId);
+                        sid, OFStatisticsType.PORT, portId);
 
-        List<NodeConnectorStatistics> ncStatistics = new PortStatisticsConverter(
-                sid, ofList).getNodeConnectorStatsList();
-        return (ncStatistics.isEmpty()) ? new NodeConnectorStatistics()
-                : ncStatistics.get(0);
+        List<NodeConnectorStatistics> ncStatistics = new PortStatisticsConverter(sid, ofList)
+                .getNodeConnectorStatsList();
+        return (ncStatistics.isEmpty()) ? new NodeConnectorStatistics() : ncStatistics.get(0);
     }
 
     @Override
-    public List<NodeConnectorStatistics> readAllNodeConnector(
-            String containerName, Node node, boolean cached) {
+    public List<NodeConnectorStatistics> readAllNodeConnector(String containerName, Node node, boolean cached) {
 
         long sid = (Long) node.getID();
         List<OFStatistics> ofList = (cached == true) ? statsMgr
                 .getOFPortStatistics(sid) : statsMgr.queryStatistics(sid,
-                OFStatisticsType.FLOW, null);
+                        OFStatisticsType.FLOW, null);
 
-        List<OFStatistics> filteredList = filterPortListPerContainer(
-                containerName, sid, ofList);
+        List<OFStatistics> filteredList = filterPortListPerContainer(containerName, sid, ofList);
 
-        return new PortStatisticsConverter(sid, filteredList)
-                .getNodeConnectorStatsList();
+        return new PortStatisticsConverter(sid, filteredList).getNodeConnectorStatsList();
     }
 
     @Override
@@ -418,4 +534,101 @@ public class ReadServiceFilter implements IPluginReadServiceFilter,
         return statsMgr.getTransmitRate(switchId, port);
     }
 
+    @Override
+    public NodeTableStatistics readNodeTable(String containerName,
+            NodeTable table, boolean cached) {
+        if (!containerOwnsNodeTable(containerName, table)) {
+            return null;
+        }
+        Node node = table.getNode();
+        long sid = (Long) node.getID();
+        Byte tableId = (Byte) table.getID();
+        List<OFStatistics> ofList = (cached == true) ? statsMgr.getOFTableStatistics(sid, tableId) :
+            statsMgr.queryStatistics(sid, OFStatisticsType.TABLE, tableId);
+
+        List<NodeTableStatistics> ntStatistics = new TableStatisticsConverter(sid, ofList).getNodeTableStatsList();
+
+        return (ntStatistics.isEmpty()) ? new NodeTableStatistics() : ntStatistics.get(0);
+    }
+
+    @Override
+    public List<NodeTableStatistics> readAllNodeTable(String containerName, Node node, boolean cached) {
+        long sid = (Long) node.getID();
+        List<OFStatistics> ofList = (cached == true) ?
+                statsMgr.getOFTableStatistics(sid) : statsMgr.queryStatistics(sid, OFStatisticsType.FLOW, null);
+
+        List<OFStatistics> filteredList = filterTableListPerContainer(containerName, sid, ofList);
+
+        return new TableStatisticsConverter(sid, filteredList).getNodeTableStatsList();
+    }
+
+    @Override
+    public void descriptionStatisticsRefreshed(Long switchId, List<OFStatistics> description) {
+        String container;
+        IReadFilterInternalListener listener;
+        Node node = NodeCreator.createOFNode(switchId);
+        NodeDescription nodeDescription = new DescStatisticsConverter(description).getHwDescription();
+        for (Map.Entry<String, IReadFilterInternalListener> l : readFilterInternalListeners.entrySet()) {
+            container = l.getKey();
+            listener = l.getValue();
+            if (container == GlobalConstants.DEFAULT.toString()
+                    || (containerToNode.containsKey(container) && containerToNode.get(container).contains(node))) {
+                listener.nodeDescriptionStatisticsUpdated(node, nodeDescription);
+            }
+        }
+    }
+
+    @Override
+    public void flowStatisticsRefreshed(Long switchId, List<OFStatistics> flows) {
+        String container;
+        IReadFilterInternalListener listener;
+        Node node = NodeCreator.createOFNode(switchId);
+        for (Map.Entry<String, IReadFilterInternalListener> l : readFilterInternalListeners.entrySet()) {
+            container = l.getKey();
+            listener = l.getValue();
+
+            // Convert and filter the statistics per container
+            List<FlowOnNode> flowOnNodeList = new FlowStatisticsConverter(flows).getFlowOnNodeList(node);
+            flowOnNodeList = filterFlowListPerContainer(container, node, flowOnNodeList);
+
+            // notify listeners
+            listener.nodeFlowStatisticsUpdated(node, flowOnNodeList);
+        }
+    }
+
+    @Override
+    public void portStatisticsRefreshed(Long switchId, List<OFStatistics> ports) {
+        String container;
+        IReadFilterInternalListener listener;
+        Node node = NodeCreator.createOFNode(switchId);
+        for (Map.Entry<String, IReadFilterInternalListener> l : readFilterInternalListeners.entrySet()) {
+            container = l.getKey();
+            listener = l.getValue();
+
+            // Convert and filter the statistics per container
+            List<OFStatistics> filteredPorts = filterPortListPerContainer(container, switchId, ports);
+            List<NodeConnectorStatistics> ncStatsList = new PortStatisticsConverter(switchId, filteredPorts)
+                    .getNodeConnectorStatsList();
+
+            // notify listeners
+            listener.nodeConnectorStatisticsUpdated(node, ncStatsList);
+        }
+    }
+
+    @Override
+    public void tableStatisticsRefreshed(Long switchId, List<OFStatistics> tables) {
+        String container;
+        Node node = NodeCreator.createOFNode(switchId);
+        for (Map.Entry<String, IReadFilterInternalListener> l : readFilterInternalListeners.entrySet()) {
+            container = l.getKey();
+
+            // Convert and filter the statistics per container
+            List<OFStatistics> filteredList = filterTableListPerContainer(container, switchId, tables);
+            List<NodeTableStatistics> tableStatsList = new TableStatisticsConverter(switchId, filteredList)
+                    .getNodeTableStatsList();
+
+            // notify listeners
+            l.getValue().nodeTableStatisticsUpdated(node, tableStatsList);
+        }
+    }
 }