[ONOS-3851] Initial implementation of Web GUI of CPMan

- Revise chart model to provide default label
- Visualize control message stats per device

Change-Id: I88b8e63ce92114907bba185b1906569fa8cc0b83
diff --git a/apps/cpman/api/src/main/java/org/onosproject/cpman/ControlLoadSnapshot.java b/apps/cpman/api/src/main/java/org/onosproject/cpman/ControlLoadSnapshot.java
index 36e436a..40c1d04 100644
--- a/apps/cpman/api/src/main/java/org/onosproject/cpman/ControlLoadSnapshot.java
+++ b/apps/cpman/api/src/main/java/org/onosproject/cpman/ControlLoadSnapshot.java
@@ -17,6 +17,7 @@
 
 import com.google.common.base.MoreObjects;
 
+import java.util.Arrays;
 import java.util.Objects;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
@@ -29,13 +30,14 @@
     private final long latest;
     private final long average;
     private final long time;
+    private long[] recent;
 
     /**
      * Instantiates a new control metric response with given latest, average, time.
      *
-     * @param latest latest value of control metric
+     * @param latest  latest value of control metric
      * @param average average value of control metric
-     * @param time last logging time fo control metric
+     * @param time    last logging time of control metric
      */
     public ControlLoadSnapshot(long latest, long average, long time) {
         this.latest = latest;
@@ -44,6 +46,22 @@
     }
 
     /**
+     * Instantiates a new control metric response with given latest, average, time,
+     * recent values.
+     *
+     * @param latest  latest value of control metric
+     * @param average average value of control metric
+     * @param time last logging time of control metric
+     * @param recent a set of historical data
+     */
+    public ControlLoadSnapshot(long latest, long average, long time, long[] recent) {
+        this.latest = latest;
+        this.average = average;
+        this.time = time;
+        this.recent = recent;
+    }
+
+    /**
      * Returns latest value of control metric.
      *
      * @return latest value of control metric
@@ -70,9 +88,18 @@
         return average;
     }
 
+    /**
+     * Returns a set of historical recent of control metric.
+     *
+     * @return a set of historical recent of control metric
+     */
+    public long[] recent() {
+        return recent;
+    }
+
     @Override
     public int hashCode() {
-        return Objects.hash(latest, average, time);
+        return Objects.hash(latest, average, time, recent);
     }
 
     @Override
@@ -84,7 +111,8 @@
             final ControlLoadSnapshot other = (ControlLoadSnapshot) obj;
             return Objects.equals(this.latest, other.latest) &&
                     Objects.equals(this.average, other.average) &&
-                    Objects.equals(this.time, other.time);
+                    Objects.equals(this.time, other.time) &&
+                    Arrays.equals(this.recent, other.recent);
         }
         return false;
     }
@@ -95,7 +123,9 @@
         helper = toStringHelper(this)
                 .add("latest", latest)
                 .add("average", average)
-                .add("time", time);
+                .add("time", time)
+                .add("recent", recent);
+
         return helper.toString();
     }
 }
diff --git a/apps/cpman/app/src/main/java/org/onosproject/cpman/gui/CpmanViewMessageHandler.java b/apps/cpman/app/src/main/java/org/onosproject/cpman/gui/CpmanViewMessageHandler.java
index 7d0785f3..2d4e28f 100644
--- a/apps/cpman/app/src/main/java/org/onosproject/cpman/gui/CpmanViewMessageHandler.java
+++ b/apps/cpman/app/src/main/java/org/onosproject/cpman/gui/CpmanViewMessageHandler.java
@@ -16,44 +16,122 @@
 package org.onosproject.cpman.gui;
 
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.joda.time.LocalDateTime;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cpman.ControlLoadSnapshot;
+import org.onosproject.cpman.ControlMetricType;
+import org.onosproject.cpman.ControlPlaneMonitorService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
 import org.onosproject.ui.RequestHandler;
 import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.chart.ChartModel;
+import org.onosproject.ui.chart.ChartRequestHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.Collection;
-import java.util.Random;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.onosproject.cpman.ControlResource.CONTROL_MESSAGE_METRICS;
+import static org.onosproject.cpman.ControlResource.Type.CONTROL_MESSAGE;
 
 /**
  * CpmanViewMessageHandler class implementation.
  */
 public class CpmanViewMessageHandler extends UiMessageHandler {
 
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
     private static final String CPMAN_DATA_REQ = "cpmanDataRequest";
     private static final String CPMAN_DATA_RESP = "cpmanDataResponse";
+    private static final String CPMANS = "cpmans";
 
-    private static final String RANDOM = "random";
+    // TODO: we assume that server side always returns 60 data points
+    // to feed 1 hour time slots, later this should make to be configurable
+    private static final int NUM_OF_DATA_POINTS = 60;
+
+    private static final int MILLI_CONV_UNIT = 1000;
 
     @Override
     protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(
-                new CpmanDataRequestHandler()
+                new ControlMessageRequest()
         );
     }
 
-    // handler for sample data requests
-    private final class CpmanDataRequestHandler extends RequestHandler {
+    private final class ControlMessageRequest extends ChartRequestHandler {
 
-        private CpmanDataRequestHandler() {
-            super(CPMAN_DATA_REQ);
+        private ControlMessageRequest() {
+            super(CPMAN_DATA_REQ, CPMAN_DATA_RESP, CPMANS);
         }
 
         @Override
-        public void process(long sid, ObjectNode payload) {
-            ObjectNode result = objectNode();
-            Random random = new Random();
-            result.put(RANDOM, random.nextInt(50) + 1);
+        protected String[] getSeries() {
+            return CONTROL_MESSAGE_METRICS.stream().map(type ->
+                    StringUtils.lowerCase(type.name())).toArray(String[]::new);
+        }
 
-            sendMessage(CPMAN_DATA_RESP, 0, result);
+        @Override
+        protected void populateChart(ChartModel cm, ObjectNode payload) {
+            String uri = string(payload, "devId");
+            if (!Strings.isNullOrEmpty(uri)) {
+                Map<ControlMetricType, Long[]> data = Maps.newHashMap();
+                DeviceId deviceId = DeviceId.deviceId(uri);
+                ClusterService cs = get(ClusterService.class);
+                ControlPlaneMonitorService cpms = get(ControlPlaneMonitorService.class);
+
+                if (cpms.availableResources(CONTROL_MESSAGE).contains(deviceId.toString())) {
+                    LocalDateTime ldt = null;
+
+                    try {
+                        for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
+                            ControlLoadSnapshot cls = cpms.getLoad(cs.getLocalNode().id(),
+                                    cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES,
+                                    Optional.of(deviceId)).get();
+                            data.put(cmt, ArrayUtils.toObject(cls.recent()));
+                            if (ldt == null) {
+                                ldt = new LocalDateTime(cls.time() * MILLI_CONV_UNIT);
+                            }
+                        }
+
+                        for (int i = 0; i < NUM_OF_DATA_POINTS; i++) {
+                            Map<String, Long> local = Maps.newHashMap();
+                            for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
+                                local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt)[i]);
+                            }
+
+                            local.put(LABEL, ldt.minusMinutes(NUM_OF_DATA_POINTS - i).toDateTime().getMillis());
+
+                            populateMetric(cm.addDataPoint(ldt.minusMinutes(NUM_OF_DATA_POINTS - i)
+                                    .toDateTime().getMillis()), local);
+                        }
+
+                    } catch (InterruptedException | ExecutionException e) {
+                        log.warn(e.getMessage());
+                    }
+                }
+            } else {
+                DeviceService ds = get(DeviceService.class);
+                ds.getAvailableDevices();
+            }
+        }
+
+        private void populateAllDevs(ChartModel.DataPoint dataPoint, Map<String, Long> data) {
+
+        }
+
+        private void populateMetric(ChartModel.DataPoint dataPoint,
+                                    Map<String, Long> data) {
+            data.forEach((k, v) -> dataPoint.data(k, v.doubleValue()));
         }
     }
 }
diff --git a/apps/cpman/app/src/main/java/org/onosproject/cpman/impl/ControlPlaneManager.java b/apps/cpman/app/src/main/java/org/onosproject/cpman/impl/ControlPlaneManager.java
index f3a29ee..5f0918b 100644
--- a/apps/cpman/app/src/main/java/org/onosproject/cpman/impl/ControlPlaneManager.java
+++ b/apps/cpman/app/src/main/java/org/onosproject/cpman/impl/ControlPlaneManager.java
@@ -81,10 +81,10 @@
             // TODO: this can be changed to switch-case if we have more than
             // one event type
             if (event.type().equals(STATS_UPDATE)) {
-                controlMessages.forEach(c -> {
+                controlMessages.forEach(c ->
                     monitorService.updateMetric(getControlMetric(c), 1,
-                            Optional.of(c.deviceId()));
-                });
+                            Optional.of(c.deviceId()))
+                );
             }
         }
     }
diff --git a/apps/cpman/app/src/main/java/org/onosproject/cpman/impl/ControlPlaneMonitor.java b/apps/cpman/app/src/main/java/org/onosproject/cpman/impl/ControlPlaneMonitor.java
index d32d2e0..dff72db 100644
--- a/apps/cpman/app/src/main/java/org/onosproject/cpman/impl/ControlPlaneMonitor.java
+++ b/apps/cpman/app/src/main/java/org/onosproject/cpman/impl/ControlPlaneMonitor.java
@@ -169,7 +169,7 @@
                 if (ctrlMsgBuf.get(deviceId.get()).keySet()
                         .containsAll(CONTROL_MESSAGE_METRICS)) {
                     updateControlMessages(ctrlMsgBuf.get(deviceId.get()), deviceId.get());
-                    ctrlMsgBuf.get(deviceId.get());
+                    ctrlMsgBuf.clear();
                 }
             }
         } else {
@@ -327,8 +327,10 @@
      */
     private void updateNetworkMetrics(Map<ControlMetricType, Double> metricMap,
                                       String resourceName) {
-        networkMetricsMap.putIfAbsent(resourceName, genMDbBuilder(resourceName,
-                Type.NETWORK, NETWORK_METRICS));
+        if (!networkMetricsMap.containsKey(resourceName)) {
+            networkMetricsMap.put(resourceName, genMDbBuilder(resourceName,
+                    Type.NETWORK, NETWORK_METRICS));
+        }
         networkMetricsMap.get(resourceName).updateMetrics(convertMap(metricMap));
     }
 
@@ -340,8 +342,10 @@
      */
     private void updateDiskMetrics(Map<ControlMetricType, Double> metricMap,
                                    String resourceName) {
-        diskMetricsMap.putIfAbsent(resourceName, genMDbBuilder(resourceName,
-                Type.DISK, DISK_METRICS));
+        if (!diskMetricsMap.containsKey(resourceName)) {
+            diskMetricsMap.put(resourceName, genMDbBuilder(resourceName,
+                    Type.DISK, DISK_METRICS));
+        }
         diskMetricsMap.get(resourceName).updateMetrics(convertMap(metricMap));
     }
 
@@ -353,8 +357,10 @@
      */
     private void updateControlMessages(Map<ControlMetricType, Double> metricMap,
                                        DeviceId deviceId) {
-        controlMessageMap.putIfAbsent(deviceId, genMDbBuilder(deviceId.toString(),
-                Type.CONTROL_MESSAGE, CONTROL_MESSAGE_METRICS));
+        if (!controlMessageMap.containsKey(deviceId)) {
+            controlMessageMap.put(deviceId, genMDbBuilder(deviceId.toString(),
+                    Type.CONTROL_MESSAGE, CONTROL_MESSAGE_METRICS));
+        }
         controlMessageMap.get(deviceId).updateMetrics(convertMap(metricMap));
     }
 
@@ -478,7 +484,9 @@
      */
     private ControlLoadSnapshot snapshot(ControlLoad cl, int duration, TimeUnit unit) {
         if (cl != null) {
-            return new ControlLoadSnapshot(cl.latest(), cl.average(duration, unit), cl.time());
+
+            return new ControlLoadSnapshot(cl.latest(), cl.average(duration, unit),
+                    cl.time(), cl.recent(duration, unit));
         }
         return null;
     }
diff --git a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.html b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.html
index d7cd832..a07e544 100644
--- a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.html
+++ b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.html
@@ -1,17 +1,8 @@
 <!-- partial HTML -->
 <div id="ov-cpman">
-    <div class="button-panel">
-        <div class="my-button" ng-click="getData()">
-            Fetch Data
-        </div>
-    </div>
-
-    <div class="data-panel">
-        <table>
-            <tr>
-                <td> Number </td>
-                <td class="number"> {{data.random}} </td>
-            </tr>
-        </table>
+    <div>
+        <canvas id="line" class="chart chart-line" chart-data="data"
+                chart-labels="labels" chart-legend="true" chart-series="series">
+        </canvas>
     </div>
 </div>
diff --git a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.js b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.js
index 65b78b3..8474629 100644
--- a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.js
+++ b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.js
@@ -20,67 +20,61 @@
 (function () {
     'use strict';
 
-    // injected refs
-    var $log, $scope, wss, ks;
+    // injected references
+    var $log, $scope, $location, ks, fs, cbs;
 
-    // constants
-    var dataReq = 'cpmanDataRequest',
-        dataResp = 'cpmanDataResponse';
+    var labels = new Array(60);
+    var data = new Array(new Array(60), new Array(60), new Array(60),
+                         new Array(60), new Array(60), new Array(60));
 
-    function addKeyBindings() {
-        var map = {
-            space: [getData, 'Fetch data from server'],
-
-            _helpFormat: [
-                ['space']
-            ]
-        };
-
-        ks.keyBindings(map);
-    }
-
-    function getData() {
-        wss.sendEvent(dataReq);
-    }
-
-    function respDataCb(data) {
-        $scope.data = data;
-        $scope.$apply();
-    }
-
-
-    angular.module('ovCpman', [])
+    angular.module('ovCpman', ["chart.js"])
         .controller('OvCpmanCtrl',
-        ['$log', '$scope', 'WebSocketService', 'KeyService',
+        ['$log', '$scope', '$location', 'FnService', 'ChartBuilderService',
 
-        function (_$log_, _$scope_, _wss_, _ks_) {
+        function (_$log_, _$scope_, _$location_, _fs_, _cbs_) {
+            var params;
             $log = _$log_;
             $scope = _$scope_;
-            wss = _wss_;
-            ks = _ks_;
+            $location = _$location_;
+            fs = _fs_;
+            cbs = _cbs_;
 
-            var handlers = {};
-            $scope.data = {};
+            params = $location.search();
+            if (params.hasOwnProperty('devId')) {
+                $scope.devId = params['devId'];
+            }
 
-            // data response handler
-            handlers[dataResp] = respDataCb;
-            wss.bindHandlers(handlers);
-
-            addKeyBindings();
-
-            // custom click handler
-            $scope.getData = getData;
-
-            // get data the first time...
-            getData();
-
-            // cleanup
-            $scope.$on('$destroy', function () {
-                wss.unbindHandlers(handlers);
-                ks.unbindKeys();
-                $log.log('OvCpmanCtrl has been destroyed');
+            cbs.buildChart({
+                scope: $scope,
+                tag: 'cpman',
+                query: params
             });
 
+            var idx = 0;
+            var date;
+            $scope.$watch('chartData', function () {
+                idx = 0;
+                if (!fs.isEmptyObject($scope.chartData)) {
+                    $scope.chartData.forEach(function (cm) {
+                        data[0][idx] = cm.inbound_packet;
+                        data[1][idx] = cm.outbound_packet;
+                        data[2][idx] = cm.flow_mod_packet;
+                        data[3][idx] = cm.flow_removed_packet;
+                        data[4][idx] = cm.request_packet;
+                        data[5][idx] = cm.reply_packet;
+                        date = new Date(cm.label);
+                        labels[idx] = date.getHours() + ":" + date.getMinutes();
+                        idx++;
+                    });
+                }
+            });
+
+            $scope.series = ['INBOUND', 'OUTBOUND', 'FLOW-MOD',
+                             'FLOW-REMOVED', 'STATS-REQUEST', 'STATS-REPLY'];
+            $scope.labels = labels;
+
+            $scope.data = data;
+
             $log.log('OvCpmanCtrl has been created');
         }]);
 
diff --git a/apps/cpman/app/src/test/java/org/onosproject/cpman/impl/MetricsDatabaseTest.java b/apps/cpman/app/src/test/java/org/onosproject/cpman/impl/MetricsDatabaseTest.java
index 110ba10..e08e2e2 100644
--- a/apps/cpman/app/src/test/java/org/onosproject/cpman/impl/MetricsDatabaseTest.java
+++ b/apps/cpman/app/src/test/java/org/onosproject/cpman/impl/MetricsDatabaseTest.java
@@ -149,11 +149,13 @@
         devMetricsMap = Maps.newHashMap();
 
         Set<DeviceId> devices = ImmutableSet.of(devId1, devId2);
-        devices.forEach(dev ->
-            devMetricsMap.putIfAbsent(dev,
-                    genMDbBuilder(type, ControlResource.CONTROL_MESSAGE_METRICS)
-                            .withResourceName(dev.toString())
-                            .build()));
+        devices.forEach(dev -> {
+            if (!devMetricsMap.containsKey(dev)) {
+                devMetricsMap.put(dev, genMDbBuilder(type, ControlResource.CONTROL_MESSAGE_METRICS)
+                        .withResourceName(dev.toString())
+                        .build());
+            }
+        });
 
         Map<String, Double> metrics1 = new HashMap<>();
         ControlResource.CONTROL_MESSAGE_METRICS.forEach(msgType ->
diff --git a/core/api/src/main/java/org/onosproject/ui/chart/ChartRequestHandler.java b/core/api/src/main/java/org/onosproject/ui/chart/ChartRequestHandler.java
index 5fba98c..2cbc05f 100644
--- a/core/api/src/main/java/org/onosproject/ui/chart/ChartRequestHandler.java
+++ b/core/api/src/main/java/org/onosproject/ui/chart/ChartRequestHandler.java
@@ -18,6 +18,10 @@
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.onosproject.ui.RequestHandler;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 /**
  * Message handler specifically for the chart views.
  */
@@ -25,6 +29,7 @@
 
     private final String respType;
     private final String nodeName;
+    protected static final String LABEL = "label";
 
     /**
      * Constructs a chart model handler for a specific graph view. When chart
@@ -61,7 +66,11 @@
      * @return an empty chart model
      */
     protected ChartModel createChartModel() {
-        return new ChartModel(getSeries());
+        List<String> series = new ArrayList<>();
+        series.addAll(Arrays.asList(getSeries()));
+        series.add(LABEL);
+        String[] seiresArray = new String[series.size()];
+        return new ChartModel(series.toArray(seiresArray));
     }
 
     /**