Revise ChartModel and add ChartRequestHandler, ChartUtils classes

Change-Id: I9c72122368c8270df9a1055845281e999d488243
diff --git a/core/api/src/main/java/org/onosproject/ui/chart/ChartModel.java b/core/api/src/main/java/org/onosproject/ui/chart/ChartModel.java
index 2d24cb4..9493acf 100644
--- a/core/api/src/main/java/org/onosproject/ui/chart/ChartModel.java
+++ b/core/api/src/main/java/org/onosproject/ui/chart/ChartModel.java
@@ -16,16 +16,20 @@
 
 package org.onosproject.ui.chart;
 
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
- * A simple model of chart data.
+ * A simple model of time series chart data.
  *
  * <p>
  * Note that this is not a full MVC type model; the expected usage pattern
@@ -35,58 +39,65 @@
  */
 public class ChartModel {
 
-    // key is series name, value is series index
-    private final Map<String, Integer> seriesMap;
-    private final DataPoint[] dataPoints;
+    private final Set<String> seriesSet;
+    private final String[] seriesArray;
+    private final List<Long> labels = Lists.newArrayList();
+    private final List<DataPoint> dataPoints = Lists.newArrayList();
 
     /**
      * Constructs a chart model with initialized series set.
      *
-     * @param size datapoints size
      * @param series a set of series
      */
-    public ChartModel(int size, String... series) {
+    public ChartModel(String... series) {
         checkNotNull(series, "series cannot be null");
         checkArgument(series.length > 0, "must be at least one series");
-        seriesMap = Maps.newConcurrentMap();
 
-        for (int index = 0; index < series.length; index++) {
-            seriesMap.put(series[index], index);
+        seriesSet = Sets.newHashSet(series);
+
+        if (seriesSet.size() != series.length) {
+            throw new IllegalArgumentException("duplicate series detected");
         }
 
-        checkArgument(size > 0, "must have at least one data point");
-        dataPoints = new DataPoint[size];
+        this.seriesArray = Arrays.copyOf(series, series.length);
     }
 
     private void checkDataPoint(DataPoint dataPoint) {
-        checkArgument(dataPoint.getSize() == seriesCount(),
+        checkArgument(dataPoint.size() == seriesCount(),
                 "data size should be equal to number of series");
     }
 
     /**
+     * Checks the validity of the given series.
+     *
+     * @param series series name
+     */
+    private void checkSeries(String series) {
+        checkNotNull(series, "must provide a series name");
+        if (!seriesSet.contains(series)) {
+            throw new IllegalArgumentException("unknown series: " + series);
+        }
+    }
+
+    /**
      * Returns the number of series in this chart model.
      *
      * @return number of series
      */
     public int seriesCount() {
-        return seriesMap.size();
+        return seriesSet.size();
     }
 
     /**
-     * Shifts all of the data points to the left,
-     * and adds a new data point to the tail of the array.
+     * Adds a data point to the chart model.
      *
-     * @param label label name
-     * @param values a set of data values
+     * @return the data point, for chaining
      */
-    public void addDataPoint(String label, Double[] values) {
-        DataPoint dp = new DataPoint(label, values);
-        checkDataPoint(dp);
-
-        for (int index = 1; index < dataPoints.length; index++) {
-            dataPoints[index - 1] = dataPoints[index];
-        }
-        dataPoints[dataPoints.length - 1] = dp;
+    public DataPoint addDataPoint(Long label) {
+        DataPoint dp = new DataPoint();
+        labels.add(label);
+        dataPoints.add(dp);
+        return dp;
     }
 
     /**
@@ -95,16 +106,34 @@
      * @return an array of series
      */
     public String[] getSeries() {
-        return seriesMap.keySet().toArray(new String[seriesMap.size()]);
+        return seriesArray;
     }
 
     /**
-     * Returns all of data points.
+     * Returns all of data points in order.
      *
      * @return an array of data points
      */
     public DataPoint[] getDataPoints() {
-        return Arrays.copyOf(dataPoints, dataPoints.length);
+        return dataPoints.toArray(new DataPoint[dataPoints.size()]);
+    }
+
+    /**
+     * Returns all of labels in order.
+     *
+     * @return an array of labels
+     */
+    public Object[] getLabels() {
+        return labels.toArray(new Long[labels.size()]);
+    }
+
+    /**
+     * Returns the number of data points in this chart model.
+     *
+     * @return number of data points
+     */
+    public int dataPointCount() {
+        return dataPoints.size();
     }
 
     /**
@@ -113,7 +142,7 @@
      * @return data point
      */
     public DataPoint getLastDataPoint() {
-        return dataPoints[dataPoints.length - 1];
+        return dataPoints.get(dataPoints.size() - 1);
     }
 
     /**
@@ -121,47 +150,52 @@
      */
     public class DataPoint {
         // values for all series
-        private final Double[] values;
-        private final String label;
+        private final Map<String, Double> data = Maps.newHashMap();
 
         /**
-         * Constructs a data point.
+         * Sets the data value for the given series of this data point.
          *
-         * @param label label name
-         * @param values a set of data values for all series
+         * @param series series name
+         * @param value value to set
+         * @return self, for chaining
          */
-        public DataPoint(String label, Double[] values) {
-            this.label = label;
-            this.values = values;
+        public DataPoint data(String series, Double value) {
+            checkSeries(series);
+            data.put(series, value);
+            return this;
         }
 
         /**
-         * Returns the label name of this data point.
+         * Returns the data value with the given series for this data point.
          *
-         * @return label name
+         * @return data value
          */
-        public String getLabel() {
-            return label;
+        public Double get(String series) {
+            return data.get(series);
+        }
+
+        /**
+         * Return the data value with the same order of series.
+         *
+         * @return an array of ordered data values
+         */
+        public Double[] getAll() {
+            Double[] value = new Double[getSeries().length];
+            int idx = 0;
+            for (String s : getSeries()) {
+                value[idx] = get(s);
+                idx++;
+            }
+            return value;
         }
 
         /**
          * Returns the size of data point.
-         * This should be identical to the size of series.
          *
-         * @return size of data point
+         * @return the size of data point
          */
-        public int getSize() {
-            return values.length;
-        }
-
-        /**
-         * Returns the value of the data point of the given series.
-         *
-         * @param series series name
-         * @return data value of a specific series
-         */
-        public Double getValue(String series) {
-            return values[seriesMap.get(series)];
+        public int size() {
+            return data.size();
         }
     }
 }
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
new file mode 100644
index 0000000..5fba98c
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/chart/ChartRequestHandler.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.ui.chart;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.ui.RequestHandler;
+
+/**
+ * Message handler specifically for the chart views.
+ */
+public abstract class ChartRequestHandler extends RequestHandler {
+
+    private final String respType;
+    private final String nodeName;
+
+    /**
+     * Constructs a chart model handler for a specific graph view. When chart
+     * requests come in, the handler will generate the appropriate chart data
+     * points and send back the response to the client.
+     *
+     * @param reqType  type of the request event
+     * @param respType type of the response event
+     * @param nodeName name of JSON node holding data point
+     */
+    public ChartRequestHandler(String reqType, String respType, String nodeName) {
+        super(reqType);
+        this.respType = respType;
+        this.nodeName = nodeName;
+    }
+
+    @Override
+    public void process(long sid, ObjectNode payload) {
+        ChartModel cm = createChartModel();
+        populateChart(cm, payload);
+
+        ObjectNode rootNode = MAPPER.createObjectNode();
+        rootNode.set(nodeName, ChartUtils.generateDataPointArrayNode(cm));
+        sendMessage(respType, 0, rootNode);
+    }
+
+    /**
+     * Creates the chart model using {@link #getSeries()}
+     * to initialize it, ready to be populated.
+     * <p>
+     * This default implementation returns a chart model for all series.
+     * </p>
+     *
+     * @return an empty chart model
+     */
+    protected ChartModel createChartModel() {
+        return new ChartModel(getSeries());
+    }
+
+    /**
+     * Subclasses should return the array of series with which to initialize
+     * their chart model.
+     *
+     * @return the series name
+     */
+    protected abstract String[] getSeries();
+
+    /**
+     * Subclasses should populate the chart model by adding
+     * {@link ChartModel.DataPoint datapoints}.
+     * <pre>
+     *     cm.addDataPoint()
+     *         .data(SERIES_ONE, ...)
+     *         .data(SERIES_TWO, ...)
+     *         ... ;
+     * </pre>
+     * The request payload is provided in case there are request filtering
+     * parameters.
+     *
+     * @param cm      the chart model
+     * @param payload request payload
+     */
+    protected abstract void populateChart(ChartModel cm, ObjectNode payload);
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/chart/ChartUtils.java b/core/api/src/main/java/org/onosproject/ui/chart/ChartUtils.java
new file mode 100644
index 0000000..959a2b0
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/chart/ChartUtils.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.ui.chart;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Provides static utility methods for dealing with charts.
+ */
+public final class ChartUtils {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    // non-instantiable
+    private ChartUtils() {
+    }
+
+    /**
+     * Generates a JSON array node from the data points of the given chart model.
+     *
+     * @param cm the chart model
+     * @return the array node representation of data points
+     */
+    public static ArrayNode generateDataPointArrayNode(ChartModel cm) {
+        ArrayNode array = MAPPER.createArrayNode();
+        for (ChartModel.DataPoint dp : cm.getDataPoints()) {
+            array.add(toJsonNode(dp, cm));
+        }
+        return array;
+    }
+
+    /**
+     * Generate a JSON node from the data point and given chart model.
+     *
+     * @param dp the data point
+     * @param cm the chart model
+     * @return the node representation of a data point with series
+     */
+    public static JsonNode toJsonNode(ChartModel.DataPoint dp, ChartModel cm) {
+        ObjectNode result = MAPPER.createObjectNode();
+        String[] series = cm.getSeries();
+        Double[] values = dp.getAll();
+        int n = series.length;
+        for (int i = 0; i < n; i++) {
+            result.put(series[i], values[i]);
+        }
+        return result;
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/ui/chart/ChartModelTest.java b/core/api/src/test/java/org/onosproject/ui/chart/ChartModelTest.java
index da1365d..0a87635 100644
--- a/core/api/src/test/java/org/onosproject/ui/chart/ChartModelTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/chart/ChartModelTest.java
@@ -17,10 +17,7 @@
 
 import org.junit.Test;
 
-import static org.hamcrest.Matchers.is;
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
 
 /**
  * Unit tests for {@link ChartModel}.
@@ -35,57 +32,97 @@
     private static final Double[] VALUES2 = {3D, 4D, 5D};
     private static final Double[] VALUES3 = {6D, 7D, 8D};
 
-    private static final String[] SERIES = {FOO, BAR, ZOO};
-
     private ChartModel cm;
 
     @Test(expected = NullPointerException.class)
     public void guardAgainstNullSeries() {
-        cm = new ChartModel(1, null);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void guardAgainstWrongDpNumber() {
-        cm = new ChartModel(0, FOO);
+        cm = new ChartModel(null);
     }
 
     @Test
     public void testSeriesCount() {
-        cm = new ChartModel(1, FOO, BAR, ZOO);
+        cm = new ChartModel(FOO, BAR, ZOO);
         assertEquals("Wrong series count", 3, cm.seriesCount());
     }
 
     @Test
+    public void emptyLabel() {
+        cm = new ChartModel(FOO, BAR, ZOO);
+        cm.addDataPoint(System.currentTimeMillis());
+
+        assertEquals("bad data point count", 1, cm.dataPointCount());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void dataPointBandSeries() {
+        cm = new ChartModel(FOO, BAR);
+
+        cm.addDataPoint(System.currentTimeMillis())
+                .data(ZOO, VALUES3[0]);
+    }
+
+    @Test
     public void testAddDataPoint() {
-        cm = new ChartModel(2, FOO, BAR, ZOO);
+        cm = new ChartModel(FOO, BAR, ZOO);
 
-        cm.addDataPoint("1", VALUES1);
-        cm.addDataPoint("2", VALUES2);
+        long time = System.currentTimeMillis();
 
-        assertEquals("Wrong result", "1", cm.getDataPoints()[0].getLabel());
-        assertEquals("Wrong result", "2", cm.getDataPoints()[1].getLabel());
+        cm.addDataPoint(time)
+                .data(FOO, VALUES1[0])
+                .data(BAR, VALUES2[0])
+                .data(ZOO, VALUES3[0]);
 
-        cm.addDataPoint("3", VALUES3);
+        cm.addDataPoint(time + 1)
+                .data(FOO, VALUES1[1])
+                .data(BAR, VALUES2[1])
+                .data(ZOO, VALUES3[1]);
 
-        assertEquals("Wrong result", "2", cm.getDataPoints()[0].getLabel());
-        assertEquals("Wrong result", "3", cm.getDataPoints()[1].getLabel());
+        cm.addDataPoint(time + 2)
+                .data(FOO, VALUES1[2])
+                .data(BAR, VALUES2[2])
+                .data(ZOO, VALUES3[2]);
+
+        assertEquals("Wrong result", 3, cm.getDataPoints()[0].size());
+        assertEquals("Wrong result", 3, cm.getDataPoints()[1].size());
+        assertEquals("Wrong result", 3, cm.getDataPoints()[2].size());
+        assertEquals("Wrong result", 3, cm.getDataPoints().length);
     }
 
     @Test
-    public void testGetData() {
-        cm = new ChartModel(2, FOO, BAR, ZOO);
+    public void testGetDataPoint() {
+        cm = new ChartModel(FOO, BAR);
 
-        cm.addDataPoint("1", VALUES1);
-        assertThat(cm.getLastDataPoint().getValue(ZOO), is(2D));
+        long time = System.currentTimeMillis();
 
-        cm.addDataPoint("2", VALUES2);
-        assertThat(cm.getLastDataPoint().getValue(BAR), is(4D));
+        cm.addDataPoint(time)
+                .data(FOO, VALUES1[0])
+                .data(BAR, VALUES2[0]);
+
+        cm.addDataPoint(time + 1)
+                .data(FOO, VALUES1[1])
+                .data(BAR, VALUES2[1]);
+
+        assertEquals("Wrong result", (Double) 0D, cm.getDataPoints()[0].get(FOO));
+        assertEquals("Wrong result", (Double) 1D, cm.getDataPoints()[1].get(FOO));
+        assertEquals("Wrong result", (Double) 3D, cm.getDataPoints()[0].get(BAR));
+        assertEquals("Wrong result", (Double) 4D, cm.getDataPoints()[1].get(BAR));
     }
 
     @Test
-    public void testGetSeries() {
-        cm = new ChartModel(1, FOO, BAR, ZOO);
+    public void testGetLastDataPoint() {
+        cm = new ChartModel(FOO, BAR);
 
-        assertArrayEquals("series", SERIES, cm.getSeries());
+        long time = System.currentTimeMillis();
+
+        cm.addDataPoint(time)
+                .data(FOO, VALUES1[0])
+                .data(BAR, VALUES2[0]);
+
+        cm.addDataPoint(time + 1)
+                .data(FOO, VALUES1[1])
+                .data(BAR, VALUES2[1]);
+
+        assertEquals("Wrong result", VALUES1[1], cm.getLastDataPoint().get(FOO));
+        assertEquals("Wrong result", VALUES2[1], cm.getLastDataPoint().get(BAR));
     }
 }
diff --git a/core/api/src/test/java/org/onosproject/ui/chart/ChartUtilsTest.java b/core/api/src/test/java/org/onosproject/ui/chart/ChartUtilsTest.java
new file mode 100644
index 0000000..13059d6
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/ui/chart/ChartUtilsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.ui.chart;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link ChartUtils}.
+ */
+public class ChartUtilsTest {
+
+    private static final String FOO = "foo";
+    private static final String BAR = "bar";
+
+    private static final String ARRAY_AS_STRING =
+            "[{\"foo\":1.0,\"bar\":2.0},{\"foo\":3.0,\"bar\":4.0}]";
+
+    @Test
+    public void basic() {
+        ChartModel cm = new ChartModel(FOO, BAR);
+        cm.addDataPoint(1L).data(FOO, 1D).data(BAR, 2D);
+        cm.addDataPoint(2L).data(FOO, 3D).data(BAR, 4D);
+
+        ArrayNode array = ChartUtils.generateDataPointArrayNode(cm);
+        Assert.assertEquals("wrong results", ARRAY_AS_STRING, array.toString());
+    }
+}