[ONOS-3603] Implement REST API for Group query, insert, delete

* Implement decoding feature for GroupBucketCodec and GroupCodec
* Implement GroupsWebResource
* Add unit test for GroupBucketCodec and GroupCodec
* Add unit test for GroupsWebResource
* Add group insertion json example
* Add Swagger doc

Change-Id: Ie58cba2e1af996c7b8652a55d9ef0c27207beafc
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java b/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
index e9fc7ac..d68b287 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
@@ -17,7 +17,6 @@
 
 import com.codahale.metrics.Metric;
 import com.google.common.collect.ImmutableSet;
-
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -35,11 +34,11 @@
 import org.onosproject.net.Link;
 import org.onosproject.net.Path;
 import org.onosproject.net.Port;
+import org.onosproject.net.device.PortStatistics;
 import org.onosproject.net.driver.Driver;
 import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.TableStatisticsEntry;
-import org.onosproject.net.device.PortStatistics;
 import org.onosproject.net.flow.TrafficSelector;
 import org.onosproject.net.flow.TrafficTreatment;
 import org.onosproject.net.flow.criteria.Criterion;
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/GroupBucketCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/GroupBucketCodec.java
index c710514..c3819b3 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/GroupBucketCodec.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/GroupBucketCodec.java
@@ -15,14 +15,18 @@
  */
 package org.onosproject.codec.impl;
 
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.onosproject.codec.CodecContext;
 import org.onosproject.codec.JsonCodec;
+import org.onosproject.core.DefaultGroupId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.PortNumber;
 import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.group.DefaultGroupBucket;
 import org.onosproject.net.group.GroupBucket;
 
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
 
 /**
  * Group bucket JSON codec.
@@ -36,6 +40,8 @@
     private static final String WATCH_GROUP = "watchGroup";
     private static final String PACKETS = "packets";
     private static final String BYTES = "bytes";
+    private static final String MISSING_MEMBER_MESSAGE =
+            " member is required in Group";
 
     @Override
     public ObjectNode encode(GroupBucket bucket, CodecContext context) {
@@ -61,4 +67,59 @@
 
         return result;
     }
+
+    @Override
+    public GroupBucket decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        // build traffic treatment
+        ObjectNode treatmentJson = get(json, TREATMENT);
+        TrafficTreatment trafficTreatment = null;
+        if (treatmentJson != null) {
+            JsonCodec<TrafficTreatment> treatmentCodec =
+                    context.codec(TrafficTreatment.class);
+            trafficTreatment = treatmentCodec.decode(treatmentJson, context);
+        }
+
+        // parse group type
+        String type = nullIsIllegal(json.get(TYPE), TYPE + MISSING_MEMBER_MESSAGE).asText();
+        GroupBucket groupBucket = null;
+
+        switch (type) {
+            case "SELECT":
+                // parse weight
+                int weightInt = nullIsIllegal(json.get(WEIGHT), WEIGHT + MISSING_MEMBER_MESSAGE).asInt();
+
+                groupBucket =
+                        DefaultGroupBucket.createSelectGroupBucket(trafficTreatment, (short) weightInt);
+                break;
+            case "INDIRECT":
+                groupBucket =
+                        DefaultGroupBucket.createIndirectGroupBucket(trafficTreatment);
+                break;
+            case "ALL":
+                groupBucket =
+                        DefaultGroupBucket.createAllGroupBucket(trafficTreatment);
+                break;
+            case "FAILOVER":
+                // parse watchPort
+                PortNumber watchPort = PortNumber.portNumber(nullIsIllegal(json.get(WATCH_PORT),
+                        WATCH_PORT + MISSING_MEMBER_MESSAGE).asText());
+
+                // parse watchGroup
+                int groupIdInt = nullIsIllegal(json.get(WATCH_GROUP),
+                        WATCH_GROUP + MISSING_MEMBER_MESSAGE).asInt();
+                GroupId watchGroup = new DefaultGroupId((short) groupIdInt);
+
+                groupBucket =
+                        DefaultGroupBucket.createFailoverGroupBucket(trafficTreatment, watchPort, watchGroup);
+                break;
+            default:
+                DefaultGroupBucket.createAllGroupBucket(trafficTreatment);
+        }
+
+        return groupBucket;
+    }
 }
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/GroupCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/GroupCodec.java
index a2f33ce..6a7e404 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/GroupCodec.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/GroupCodec.java
@@ -15,20 +15,40 @@
  */
 package org.onosproject.codec.impl;
 
-import org.onosproject.codec.CodecContext;
-import org.onosproject.codec.JsonCodec;
-import org.onosproject.net.group.Group;
-import org.onosproject.net.group.GroupBucket;
-
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.core.DefaultGroupId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.DefaultGroupKey;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.group.GroupKey;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
+import static org.slf4j.LoggerFactory.getLogger;
 
 /**
  * Group JSON codec.
  */
 public final class GroupCodec extends JsonCodec<Group> {
+    private final Logger log = getLogger(getClass());
+
     // JSON field names
     private static final String ID = "id";
     private static final String STATE = "state";
@@ -37,11 +57,15 @@
     private static final String BYTES = "bytes";
     private static final String REFERENCE_COUNT = "referenceCount";
     private static final String TYPE = "type";
+    private static final String GROUP_ID = "groupId";
     private static final String DEVICE_ID = "deviceId";
     private static final String APP_ID = "appId";
-    private static final String APP_COOKIE  = "appCookie";
-    private static final String GIVEN_GROUP_ID  = "givenGroupId";
+    private static final String APP_COOKIE = "appCookie";
+    private static final String GIVEN_GROUP_ID = "givenGroupId";
     private static final String BUCKETS = "buckets";
+    private static final String MISSING_MEMBER_MESSAGE =
+            " member is required in Group";
+    public static final String REST_APP_ID = "org.onosproject.rest";
 
     @Override
     public ObjectNode encode(Group group, CodecContext context) {
@@ -70,10 +94,81 @@
 
         ArrayNode buckets = context.mapper().createArrayNode();
         group.buckets().buckets().forEach(bucket -> {
-                    ObjectNode bucketJson = context.codec(GroupBucket.class).encode(bucket, context);
-                    buckets.add(bucketJson);
-                });
+            ObjectNode bucketJson = context.codec(GroupBucket.class).encode(bucket, context);
+            buckets.add(bucketJson);
+        });
         result.set(BUCKETS, buckets);
         return result;
     }
+
+    @Override
+    public Group decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        final JsonCodec<GroupBucket> groupBucketCodec = context.codec(GroupBucket.class);
+        CoreService coreService = context.getService(CoreService.class);
+
+        // parse group id
+        int groupIdInt = nullIsIllegal(json.get(GROUP_ID),
+                GROUP_ID + MISSING_MEMBER_MESSAGE).asInt();
+        GroupId groupId = new DefaultGroupId((short) groupIdInt);
+
+        // parse group key (appCookie)
+        String groupKeyStr = nullIsIllegal(json.get(APP_COOKIE),
+                APP_COOKIE + MISSING_MEMBER_MESSAGE).asText();
+        GroupKey groupKey = new DefaultGroupKey(groupKeyStr.getBytes());
+
+        // parse device id
+        DeviceId deviceId = DeviceId.deviceId(nullIsIllegal(json.get(DEVICE_ID),
+                DEVICE_ID + MISSING_MEMBER_MESSAGE).asText());
+
+        // application id
+        ApplicationId appId = coreService.registerApplication(REST_APP_ID);
+
+        // parse group type
+        String type = nullIsIllegal(json.get(TYPE),
+                TYPE + MISSING_MEMBER_MESSAGE).asText();
+        GroupDescription.Type groupType = null;
+
+        switch (type) {
+            case "SELECT":
+                groupType = Group.Type.SELECT;
+                break;
+            case "INDIRECT":
+                groupType = Group.Type.INDIRECT;
+                break;
+            case "ALL":
+                groupType = Group.Type.ALL;
+                break;
+            case "FAILOVER":
+                groupType = Group.Type.FAILOVER;
+                break;
+            default:
+                log.warn("The requested type {} is not defined for group.", type);
+                return null;
+        }
+
+        // parse group buckets
+        // TODO: make sure that INDIRECT group only has one bucket
+        GroupBuckets buckets = null;
+        List<GroupBucket> groupBucketList = new ArrayList<>();
+        JsonNode bucketsJson = json.get(BUCKETS);
+        checkNotNull(bucketsJson);
+        if (bucketsJson != null) {
+            IntStream.range(0, bucketsJson.size())
+                    .forEach(i -> {
+                        ObjectNode bucketJson = get(bucketsJson, i);
+                        bucketJson.put("type", type);
+                        groupBucketList.add(groupBucketCodec.decode(bucketJson, context));
+                    });
+            buckets = new GroupBuckets(groupBucketList);
+        }
+
+        GroupDescription groupDescription = new DefaultGroupDescription(deviceId,
+                groupType, buckets, groupKey, groupIdInt, appId);
+
+        return new DefaultGroup(groupId, groupDescription);
+    }
 }
diff --git a/core/common/src/test/java/org/onosproject/codec/impl/GroupCodecTest.java b/core/common/src/test/java/org/onosproject/codec/impl/GroupCodecTest.java
index 409f8eb..ffaefd6 100644
--- a/core/common/src/test/java/org/onosproject/codec/impl/GroupCodecTest.java
+++ b/core/common/src/test/java/org/onosproject/codec/impl/GroupCodecTest.java
@@ -15,21 +15,37 @@
  */
 package org.onosproject.codec.impl;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableList;
+import org.junit.Before;
 import org.junit.Test;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.core.CoreService;
 import org.onosproject.core.DefaultGroupId;
 import org.onosproject.net.NetTestTools;
+import org.onosproject.net.PortNumber;
 import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
 import org.onosproject.net.group.DefaultGroup;
 import org.onosproject.net.group.DefaultGroupBucket;
+import org.onosproject.net.group.Group;
 import org.onosproject.net.group.GroupBucket;
 import org.onosproject.net.group.GroupBuckets;
 import org.onosproject.net.group.GroupDescription;
 
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.io.InputStream;
 
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
 import static org.onosproject.codec.impl.GroupJsonMatcher.matchesGroup;
+import static org.onosproject.net.NetTestTools.APP_ID;
 
 /**
  * Group codec unit tests.
@@ -37,8 +53,28 @@
 
 public class GroupCodecTest {
 
+    MockCodecContext context;
+    JsonCodec<Group> groupCodec;
+    final CoreService mockCoreService = createMock(CoreService.class);
+
+    /**
+     * Sets up for each test.  Creates a context and fetches the flow rule
+     * codec.
+     */
+    @Before
+    public void setUp() {
+        context = new MockCodecContext();
+        groupCodec = context.codec(Group.class);
+        assertThat(groupCodec, notNullValue());
+
+        expect(mockCoreService.registerApplication(GroupCodec.REST_APP_ID))
+                .andReturn(APP_ID).anyTimes();
+        replay(mockCoreService);
+        context.registerService(CoreService.class, mockCoreService);
+    }
+
     @Test
-    public void codecTest() {
+    public void codecEncodeTest() {
         GroupBucket bucket1 = DefaultGroupBucket
                 .createSelectGroupBucket(DefaultTrafficTreatment.emptyTreatment());
         GroupBucket bucket2 = DefaultGroupBucket
@@ -58,4 +94,48 @@
 
         assertThat(groupJson, matchesGroup(group));
     }
+
+    @Test
+    public void codecDecodeTest() throws IOException {
+        Group group = getGroup("simple-group.json");
+        checkCommonData(group);
+
+        assertThat(group.buckets().buckets().size(), is(1));
+        GroupBucket groupBucket = group.buckets().buckets().get(0);
+        assertThat(groupBucket.type().toString(), is("ALL"));
+        assertThat(groupBucket.treatment().allInstructions().size(), is(1));
+        Instruction instruction1 = groupBucket.treatment().allInstructions().get(0);
+        assertThat(instruction1.type(), is(Instruction.Type.OUTPUT));
+        assertThat(((Instructions.OutputInstruction) instruction1).port(), is(PortNumber.portNumber(2)));
+    }
+
+    /**
+     * Checks that the data shared by all the resource is correct for a given group.
+     *
+     * @param group group to check
+     */
+    private void checkCommonData(Group group) {
+        assertThat(group.appId(), is(APP_ID));
+        assertThat(group.deviceId().toString(), is("of:0000000000000001"));
+        assertThat(group.type().toString(), is("ALL"));
+        assertThat(group.appCookie().key(), is("1".getBytes()));
+        assertThat(group.id().id(), is(1));
+    }
+
+    /**
+     * Reads in a group from the given resource and decodes it.
+     *
+     * @param resourceName resource to use to read the JSON for the rule
+     * @return decoded group
+     * @throws IOException if processing the resource fails
+     */
+    private Group getGroup(String resourceName) throws IOException {
+        InputStream jsonStream = GroupCodecTest.class
+                .getResourceAsStream(resourceName);
+        JsonNode json = context.mapper().readTree(jsonStream);
+        assertThat(json, notNullValue());
+        Group group = groupCodec.decode((ObjectNode) json, context);
+        assertThat(group, notNullValue());
+        return group;
+    }
 }
diff --git a/core/common/src/test/resources/org/onosproject/codec/impl/simple-group.json b/core/common/src/test/resources/org/onosproject/codec/impl/simple-group.json
new file mode 100644
index 0000000..675f244
--- /dev/null
+++ b/core/common/src/test/resources/org/onosproject/codec/impl/simple-group.json
@@ -0,0 +1,18 @@
+{
+  "type": "ALL",
+  "deviceId": "of:0000000000000001",
+  "appCookie": "1",
+  "groupId": "1",
+  "buckets": [
+    {
+      "treatment": {
+        "instructions": [
+          {
+            "type": "OUTPUT",
+            "port": 2
+          }
+        ]
+      }
+    }
+  ]
+}
\ No newline at end of file