[ONOS-4016] Add Region codec with unit test for Region REST API

Change-Id: Ib7d6daa3adf8b23bea681e7bd3ef64839be65d13
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 de74801..5bea2d3 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
@@ -58,6 +58,7 @@
 import org.onosproject.net.meter.Band;
 import org.onosproject.net.meter.Meter;
 import org.onosproject.net.meter.MeterRequest;
+import org.onosproject.net.region.Region;
 import org.onosproject.net.statistic.Load;
 import org.onosproject.net.topology.Topology;
 import org.onosproject.net.topology.TopologyCluster;
@@ -121,6 +122,7 @@
         registerCodec(NextObjective.class, new NextObjectiveCodec());
         registerCodec(McastRoute.class, new McastRouteCodec());
         registerCodec(DeviceKey.class, new DeviceKeyCodec());
+        registerCodec(Region.class, new RegionCodec());
         log.info("Started");
     }
 
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/RegionCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/RegionCodec.java
new file mode 100644
index 0000000..fd06101
--- /dev/null
+++ b/core/common/src/main/java/org/onosproject/codec/impl/RegionCodec.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2016 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.codec.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.Sets;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.net.region.DefaultRegion;
+import org.onosproject.net.region.Region;
+import org.onosproject.net.region.RegionId;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.IntStream;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
+
+/**
+ * Codec for the Region class.
+ */
+public class RegionCodec extends JsonCodec<Region> {
+
+    // JSON field names
+    private static final String REGION_ID = "id";
+    private static final String NAME = "name";
+    private static final String TYPE = "type";
+    private static final String MASTERS = "masters";
+    private static final String NODE_ID = "nodeId";
+    private static final String REGION_NOT_NULL_MSG = "Region cannot be null";
+    private static final String MISSING_MEMBER_MESSAGE = " member is required in Region";
+
+    private static final BiMap<String, Region.Type> REGION_TYPE_MAP = HashBiMap.create();
+
+    static {
+        // key is String representation of Region.Type
+        // value is Region.Type
+        REGION_TYPE_MAP.put("CONTINENT", Region.Type.CONTINENT);
+        REGION_TYPE_MAP.put("COUNTRY", Region.Type.COUNTRY);
+        REGION_TYPE_MAP.put("METRO", Region.Type.METRO);
+        REGION_TYPE_MAP.put("CAMPUS", Region.Type.CAMPUS);
+        REGION_TYPE_MAP.put("BUILDING", Region.Type.BUILDING);
+        REGION_TYPE_MAP.put("FLOOR", Region.Type.FLOOR);
+        REGION_TYPE_MAP.put("ROOM", Region.Type.ROOM);
+        REGION_TYPE_MAP.put("RACK", Region.Type.RACK);
+        REGION_TYPE_MAP.put("LOGICAL_GROUP", Region.Type.LOGICAL_GROUP);
+    }
+
+    @Override
+    public ObjectNode encode(Region region, CodecContext context) {
+        checkNotNull(region, REGION_NOT_NULL_MSG);
+
+        ObjectNode result = context.mapper().createObjectNode()
+                .put(REGION_ID, region.id().toString())
+                .put(NAME, region.name())
+                .put(TYPE, region.type().toString());
+
+        ArrayNode masters = context.mapper().createArrayNode();
+
+        region.masters().forEach(sets -> {
+            ArrayNode setsJson = context.mapper().createArrayNode();
+            sets.forEach(nodeId -> setsJson.add(nodeId.toString()));
+            masters.add(setsJson);
+        });
+        result.set(MASTERS, masters);
+        return result;
+    }
+
+    @Override
+    public Region decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        // parse masters
+        List<Set<NodeId>> masters = new ArrayList<>();
+        JsonNode mastersJson = json.get(MASTERS);
+        checkNotNull(mastersJson);
+
+        if (mastersJson != null) {
+            IntStream.range(0, mastersJson.size()).forEach(i -> {
+                ObjectNode setsJson = get(mastersJson, i);
+                final Set<NodeId> nodeIds = Sets.newHashSet();
+                if (setsJson != null && setsJson.isArray()) {
+                    Set<NodeId> localNodeIds = Sets.newHashSet();
+                    IntStream.range(0, mastersJson.size()).forEach(j -> {
+                        ObjectNode nodeIdJson = get(setsJson, j);
+                        localNodeIds.add(decodeNodeId(nodeIdJson));
+                    });
+                    nodeIds.addAll(localNodeIds);
+                }
+                masters.add(nodeIds);
+            });
+        }
+
+        // parse region id
+        RegionId regionId = RegionId.regionId(nullIsIllegal(json.get(REGION_ID),
+                REGION_ID + MISSING_MEMBER_MESSAGE).asText());
+
+        // parse region name
+        String name = nullIsIllegal(json.get(NAME), NAME +
+                MISSING_MEMBER_MESSAGE).asText();
+
+        // parse region type
+        String typeText = nullIsIllegal(json.get(TYPE), TYPE +
+                MISSING_MEMBER_MESSAGE).asText();
+
+        Region.Type type = REGION_TYPE_MAP.get(typeText);
+
+        return new DefaultRegion(regionId, name, type, masters);
+    }
+
+    /**
+     * Decodes node id json to node id object.
+     *
+     * @param json json object
+     * @return decoded node id object
+     */
+    private NodeId decodeNodeId(ObjectNode json) {
+        NodeId nodeId = NodeId.nodeId(nullIsIllegal(json, NODE_ID +
+                MISSING_MEMBER_MESSAGE).asText());
+
+        return nodeId;
+    }
+}
diff --git a/core/common/src/test/java/org/onosproject/codec/impl/RegionCodecTest.java b/core/common/src/test/java/org/onosproject/codec/impl/RegionCodecTest.java
new file mode 100644
index 0000000..8b8d276
--- /dev/null
+++ b/core/common/src/test/java/org/onosproject/codec/impl/RegionCodecTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 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.codec.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.hamcrest.MatcherAssert;
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.region.DefaultRegion;
+import org.onosproject.net.region.Region;
+import org.onosproject.net.region.RegionId;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Set;
+
+import static org.easymock.EasyMock.createMock;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertThat;
+import static org.onosproject.codec.impl.RegionJsonMatcher.matchesRegion;
+
+/**
+ * Unit tests for region codec.
+ */
+public class RegionCodecTest {
+
+    MockCodecContext context;
+    JsonCodec<Region> regionCodec;
+    final CoreService mockCoreService = createMock(CoreService.class);
+
+    @Before
+    public void setUp() {
+        context = new MockCodecContext();
+        regionCodec = context.codec(Region.class);
+        assertThat(regionCodec, notNullValue());
+    }
+
+    /**
+     * Tests encoding of a Region object.
+     */
+    @Test
+    public void testRegionEncode() {
+        NodeId nodeId1 = NodeId.nodeId("1");
+        NodeId nodeId2 = NodeId.nodeId("2");
+        NodeId nodeId3 = NodeId.nodeId("3");
+        NodeId nodeId4 = NodeId.nodeId("4");
+
+        Set<NodeId> set1 = ImmutableSet.of(nodeId1);
+        Set<NodeId> set2 = ImmutableSet.of(nodeId1, nodeId2);
+        Set<NodeId> set3 = ImmutableSet.of(nodeId1, nodeId2, nodeId3);
+        Set<NodeId> set4 = ImmutableSet.of(nodeId1, nodeId2, nodeId3, nodeId4);
+        List<Set<NodeId>> masters = ImmutableList.of(set1, set2, set3, set4);
+
+        RegionId regionId = RegionId.regionId("1");
+        String name = "foo";
+        Region.Type type = Region.Type.ROOM;
+
+        Region region = new DefaultRegion(regionId, name, type, masters);
+
+        ObjectNode regionJson = regionCodec.encode(region, context);
+        assertThat(regionJson, matchesRegion(region));
+    }
+
+    /**
+     * Tests decoding of a json object.
+     */
+    @Test
+    public void testRegionDecode() throws IOException {
+        Region region = getRegion("Region.json");
+        checkCommonData(region);
+
+        assertThat(region.masters().size(), is(2));
+    }
+
+    /**
+     * Checks that the data shared by all the resource is correct for a given region.
+     *
+     * @param region region to check
+     */
+    private void checkCommonData(Region region) {
+        assertThat(region.id().toString(), is("1"));
+        assertThat(region.type().toString(), is("ROOM"));
+        assertThat(region.name(), is("foo"));
+    }
+
+    /**
+     * Reads in a region from the given resource and decodes it.
+     *
+     * @param resourceName resource to use to read the JSON for the rule
+     * @return decoded region
+     * @throws IOException if processing the resource fails
+     */
+    private Region getRegion(String resourceName) throws IOException {
+        InputStream jsonStream = RegionCodecTest.class.getResourceAsStream(resourceName);
+        JsonNode json = context.mapper().readTree(jsonStream);
+        MatcherAssert.assertThat(json, notNullValue());
+        Region region = regionCodec.decode((ObjectNode) json, context);
+        assertThat(region, notNullValue());
+        return region;
+    }
+}
diff --git a/core/common/src/test/java/org/onosproject/codec/impl/RegionJsonMatcher.java b/core/common/src/test/java/org/onosproject/codec/impl/RegionJsonMatcher.java
new file mode 100644
index 0000000..b87162b
--- /dev/null
+++ b/core/common/src/test/java/org/onosproject/codec/impl/RegionJsonMatcher.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016 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.codec.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.onosproject.net.region.Region;
+
+/**
+ * Hamcrest matcher for region.
+ */
+public final class RegionJsonMatcher extends TypeSafeDiagnosingMatcher<JsonNode> {
+
+    private final Region region;
+
+    private RegionJsonMatcher(Region region) {
+        this.region = region;
+    }
+
+    @Override
+    protected boolean matchesSafely(JsonNode jsonRegion, Description description) {
+        // check id
+        String jsonRegionId = jsonRegion.get("id").asText();
+        String regionId = region.id().toString();
+        if (!jsonRegionId.equals(regionId)) {
+            description.appendText("region id was " + jsonRegionId);
+            return false;
+        }
+
+        // check type
+        String jsonType = jsonRegion.get("type").asText();
+        String type = region.type().toString();
+        if (!jsonType.equals(type)) {
+            description.appendText("type was " + jsonType);
+            return false;
+        }
+
+        // check name
+        String jsonName = jsonRegion.get("name").asText();
+        String name = region.name();
+        if (!jsonName.equals(name)) {
+            description.appendText("name was " + jsonName);
+            return false;
+        }
+
+        // check size of master array
+        JsonNode jsonMasters = jsonRegion.get("masters");
+        if (jsonMasters.size() != region.masters().size()) {
+            description.appendText("masters size was " + jsonMasters.size());
+            return false;
+        }
+
+        // TODO: check the content inside masters
+
+        return true;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText(region.toString());
+    }
+
+    /**
+     * Factory to allocate a region matcher.
+     *
+     * @param region region object we are looking for
+     * @return matcher
+     */
+    public static RegionJsonMatcher matchesRegion(Region region) {
+        return new RegionJsonMatcher(region);
+    }
+}
diff --git a/core/common/src/test/resources/org/onosproject/codec/impl/Region.json b/core/common/src/test/resources/org/onosproject/codec/impl/Region.json
new file mode 100644
index 0000000..087f66e
--- /dev/null
+++ b/core/common/src/test/resources/org/onosproject/codec/impl/Region.json
@@ -0,0 +1,13 @@
+{
+  "id": 1,
+  "type": "ROOM",
+  "name": "foo",
+  "masters": [
+    [
+      "1"
+    ],
+    [
+      "1", "2"
+    ]
+  ]
+}
\ No newline at end of file