[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);
+    }
 }