[ONOS-4016] Implement Region administration REST API

- Impelent Region management REST API
- Add unit test for Region management REST API
- Add swagger docs for Region management REST API
- Add SCR Component and Service annotation for RegionManager

Change-Id: I042e92ed7144d596659b779a59239afba832ca62
diff --git a/core/net/src/main/java/org/onosproject/net/region/impl/RegionManager.java b/core/net/src/main/java/org/onosproject/net/region/impl/RegionManager.java
index fcb1d6a..52634ea 100644
--- a/core/net/src/main/java/org/onosproject/net/region/impl/RegionManager.java
+++ b/core/net/src/main/java/org/onosproject/net/region/impl/RegionManager.java
@@ -17,9 +17,11 @@
 package org.onosproject.net.region.impl;
 
 import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
 import org.onosproject.cluster.NodeId;
 import org.onosproject.event.AbstractListenerManager;
 import org.onosproject.net.DeviceId;
@@ -45,6 +47,8 @@
 /**
  * Provides implementation of the region service APIs.
  */
+@Component(immediate = true)
+@Service
 public class RegionManager extends AbstractListenerManager<RegionEvent, RegionListener>
         implements RegionAdminService, RegionService {
 
diff --git a/web/api/src/main/java/org/onosproject/rest/resources/CoreWebApplication.java b/web/api/src/main/java/org/onosproject/rest/resources/CoreWebApplication.java
index 59652f9..1fa6f79 100644
--- a/web/api/src/main/java/org/onosproject/rest/resources/CoreWebApplication.java
+++ b/web/api/src/main/java/org/onosproject/rest/resources/CoreWebApplication.java
@@ -46,7 +46,8 @@
                 MetricsWebResource.class,
                 FlowObjectiveWebResource.class,
                 MulticastRouteWebResource.class,
-                DeviceKeyWebResource.class
+                DeviceKeyWebResource.class,
+                RegionsWebResource.class
         );
     }
 }
diff --git a/web/api/src/main/java/org/onosproject/rest/resources/RegionsWebResource.java b/web/api/src/main/java/org/onosproject/rest/resources/RegionsWebResource.java
new file mode 100644
index 0000000..dab1995
--- /dev/null
+++ b/web/api/src/main/java/org/onosproject/rest/resources/RegionsWebResource.java
@@ -0,0 +1,256 @@
+/*
+ * 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.rest.resources;
+
+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.Sets;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.region.Region;
+import org.onosproject.net.region.RegionAdminService;
+import org.onosproject.net.region.RegionId;
+import org.onosproject.net.region.RegionService;
+import org.onosproject.rest.AbstractWebResource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Set;
+
+import static org.onlab.util.Tools.nullIsNotFound;
+
+/**
+ * Manages region and device membership.
+ */
+@Path("regions")
+public class RegionsWebResource extends AbstractWebResource {
+    private final RegionService regionService = get(RegionService.class);
+    private final RegionAdminService regionAdminService = get(RegionAdminService.class);
+
+    private static final String REGION_NOT_FOUND = "Region is not found for ";
+    private static final String REGION_INVALID = "Invalid regionId in region update request";
+    private static final String DEVICE_IDS_INVALID = "Invalid device identifiers";
+
+    /**
+     * Returns set of all regions.
+     *
+     * @return 200 OK
+     * @onos.rsModel Regions
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getRegions() {
+        final Iterable<Region> regions = regionService.getRegions();
+        return ok(encodeArray(Region.class, "regions", regions)).build();
+    }
+
+    /**
+     * Returns the region with the specified identifier.
+     *
+     * @param regionId region identifier
+     * @return 200 OK, 404 not found
+     * @onos.rsModel Region
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{regionId}")
+    public Response getRegionById(@PathParam("regionId") String regionId) {
+        final RegionId rid = RegionId.regionId(regionId);
+        final Region region = nullIsNotFound(regionService.getRegion(rid),
+                REGION_NOT_FOUND + rid.toString());
+        return ok(codec(Region.class).encode(region, this)).build();
+    }
+
+    /**
+     * Returns the set of devices that belong to the specified region.
+     *
+     * @param regionId region identifier
+     * @return 200 OK
+     * @onos.rsModel RegionDeviceIds
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{regionId}/devices")
+    public Response getRegionDevices(@PathParam("regionId") String regionId) {
+        final RegionId rid = RegionId.regionId(regionId);
+        final Iterable<DeviceId> deviceIds = regionService.getRegionDevices(rid);
+        final ObjectNode root = mapper().createObjectNode();
+        final ArrayNode deviceIdsNode = root.putArray("deviceIds");
+        deviceIds.forEach(did -> deviceIdsNode.add(did.toString()));
+        return ok(root).build();
+    }
+
+    /**
+     * Creates a new region using the supplied JSON input stream.
+     *
+     * @param stream region JSON stream
+     * @return status of the request - CREATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel RegionPost
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response createRegion(InputStream stream) {
+        URI location;
+        try {
+            ObjectNode jsonTree = (ObjectNode) mapper().readTree(stream);
+            final Region region = codec(Region.class).decode(jsonTree, this);
+            final Region resultRegion = regionAdminService.createRegion(region.id(),
+                                region.name(), region.type(), region.masters());
+            location = new URI(resultRegion.id().id());
+        } catch (IOException | URISyntaxException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        return Response.created(location).build();
+    }
+
+    /**
+     * Updates the specified region using the supplied JSON input stream.
+     *
+     * @param regionId region identifier
+     * @param stream region JSON stream
+     * @return status of the request - UPDATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel RegionPost
+     */
+    @PUT
+    @Path("{regionId}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response updateRegion(@PathParam("regionId") String regionId,
+                                 InputStream stream) {
+        try {
+            ObjectNode jsonTree = (ObjectNode) mapper().readTree(stream);
+            JsonNode specifiedRegionId = jsonTree.get("id");
+
+            if (specifiedRegionId != null &&
+                    !specifiedRegionId.asText().equals(regionId)) {
+                throw new IllegalArgumentException(REGION_INVALID);
+            }
+
+            final Region region = codec(Region.class).decode(jsonTree, this);
+            regionAdminService.updateRegion(region.id(),
+                                region.name(), region.type(), region.masters());
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        return Response.ok().build();
+    }
+
+    /**
+     * Removes the specified region using the given region identifier.
+     *
+     * @param regionId region identifier
+     * @return 200 OK, 404 not found
+     */
+    @DELETE
+    @Path("{regionId}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response removeRegion(@PathParam("regionId") String regionId) {
+        final RegionId rid = RegionId.regionId(regionId);
+        regionAdminService.removeRegion(rid);
+        return Response.ok().build();
+    }
+
+    /**
+     * Adds the specified collection of devices to the region.
+     *
+     * @param regionId region identifier
+     * @param stream deviceIds JSON stream
+     * @return status of the request - CREATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel RegionDeviceIds
+     */
+    @POST
+    @Path("{regionId}/devices")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response addDevices(@PathParam("regionId") String regionId,
+                               InputStream stream) {
+        final RegionId rid = RegionId.regionId(regionId);
+
+        URI location;
+        try {
+            regionAdminService.addDevices(rid, extractDeviceIds(stream));
+            location = new URI(rid.id());
+        } catch (IOException | URISyntaxException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        return Response.created(location).build();
+    }
+
+    /**
+     * Removes the specified collection of devices from the region.
+     *
+     * @param regionId region identifier
+     * @param stream deviceIds JSON stream
+     * @return 200 OK, 404 not found
+     * @onos.rsModel RegionDeviceIds
+     */
+    @DELETE
+    @Path("{regionId}/devices")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response removeDevices(@PathParam("regionId") String regionId,
+                                  InputStream stream) {
+        final RegionId rid = RegionId.regionId(regionId);
+
+        try {
+            regionAdminService.removeDevices(rid, extractDeviceIds(stream));
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        return Response.ok().build();
+    }
+
+    /**
+     * Extracts device ids from a given JSON string.
+     *
+     * @param stream deviceIds JSON stream
+     * @return a set of device identifiers
+     * @throws IOException
+     */
+    private Set<DeviceId> extractDeviceIds(InputStream stream) throws IOException {
+        ObjectNode jsonTree = (ObjectNode) mapper().readTree(stream);
+        JsonNode deviceIdsJson = jsonTree.get("deviceIds");
+
+        if (deviceIdsJson == null || deviceIdsJson.size() == 0) {
+            throw new IllegalArgumentException(DEVICE_IDS_INVALID);
+        }
+
+        Set<DeviceId> deviceIds = Sets.newHashSet();
+        deviceIdsJson.forEach(did -> deviceIds.add(DeviceId.deviceId(did.asText())));
+
+        return deviceIds;
+    }
+}
diff --git a/web/api/src/main/resources/definitions/Region.json b/web/api/src/main/resources/definitions/Region.json
new file mode 100644
index 0000000..2f27258
--- /dev/null
+++ b/web/api/src/main/resources/definitions/Region.json
@@ -0,0 +1,42 @@
+{
+  "type": "object",
+  "title": "region",
+  "required": [
+    "id",
+    "name",
+    "type",
+    "masters"
+  ],
+  "properties": {
+    "id": {
+      "type": "string",
+      "example": "1"
+    },
+    "name": {
+      "type": "string",
+      "example": "region"
+    },
+    "type": {
+      "type": "string",
+      "example": "ROOM"
+    },
+    "masters": {
+      "type": "array",
+      "xml": {
+        "name": "masters",
+        "wrapped": true
+      },
+      "items": {
+        "type": "array",
+        "xml": {
+          "name": "masters",
+          "wrapped": true
+        },
+        "items": {
+          "type": "string",
+          "example": "1"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/web/api/src/main/resources/definitions/RegionDeviceIds.json b/web/api/src/main/resources/definitions/RegionDeviceIds.json
new file mode 100644
index 0000000..114e0a6
--- /dev/null
+++ b/web/api/src/main/resources/definitions/RegionDeviceIds.json
@@ -0,0 +1,20 @@
+{
+  "type": "object",
+  "title": "deviceIds",
+  "required": [
+    "deviceIds"
+  ],
+  "properties": {
+    "deviceIds": {
+      "type": "array",
+      "xml": {
+        "name": "deviceIds",
+        "wrapped": true
+      },
+      "items": {
+        "type": "string",
+        "example": "of:0000000000000001"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/web/api/src/main/resources/definitions/RegionPost.json b/web/api/src/main/resources/definitions/RegionPost.json
new file mode 100644
index 0000000..88dc91d
--- /dev/null
+++ b/web/api/src/main/resources/definitions/RegionPost.json
@@ -0,0 +1,42 @@
+{
+  "type": "object",
+  "title": "region",
+  "required": [
+    "id",
+    "name",
+    "type",
+    "masters"
+  ],
+  "properties": {
+    "id": {
+      "type": "string",
+      "example": "1"
+    },
+    "name": {
+      "type": "string",
+      "example": "region"
+    },
+    "type": {
+      "type": "string",
+      "example": "ROOM"
+    },
+    "masters": {
+      "type": "array",
+      "xml": {
+        "name": "masters",
+        "wrapped": true
+      },
+      "items": {
+        "type": "array",
+        "xml": {
+          "name": "masters",
+          "wrapped": true
+        },
+        "items": {
+          "type": "string",
+          "example": "1"
+        }
+      }
+    }
+  }
+}
diff --git a/web/api/src/main/resources/definitions/Regions.json b/web/api/src/main/resources/definitions/Regions.json
new file mode 100644
index 0000000..7ab7a11
--- /dev/null
+++ b/web/api/src/main/resources/definitions/Regions.json
@@ -0,0 +1,58 @@
+{
+  "type": "object",
+  "title": "regions",
+  "required": [
+    "regions"
+  ],
+  "properties": {
+    "regions": {
+      "type": "array",
+      "xml": {
+        "name": "regions",
+        "wrapped": true
+      },
+      "items": {
+        "type": "object",
+        "title": "region",
+        "required": [
+          "id",
+          "name",
+          "type",
+          "masters"
+        ],
+        "properties": {
+          "id": {
+            "type": "string",
+            "example": "1"
+          },
+          "name": {
+            "type": "string",
+            "example": "region"
+          },
+          "type": {
+            "type": "string",
+            "example": "ROOM"
+          },
+          "masters": {
+            "type": "array",
+            "xml": {
+              "name": "masters",
+              "wrapped": true
+            },
+            "items": {
+              "type": "array",
+              "xml": {
+                "name": "masters",
+                "wrapped": true
+              },
+              "items": {
+                "type": "string",
+                "example": "1"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/web/api/src/test/java/org/onosproject/rest/RegionsResourceTest.java b/web/api/src/test/java/org/onosproject/rest/RegionsResourceTest.java
new file mode 100644
index 0000000..620251d
--- /dev/null
+++ b/web/api/src/test/java/org/onosproject/rest/RegionsResourceTest.java
@@ -0,0 +1,470 @@
+/*
+ * 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.rest;
+
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.WebResource;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.osgi.ServiceDirectory;
+import org.onlab.osgi.TestServiceDirectory;
+import org.onlab.rest.BaseResource;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.codec.CodecService;
+import org.onosproject.codec.impl.CodecManager;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.region.Region;
+import org.onosproject.net.region.RegionAdminService;
+import org.onosproject.net.region.RegionId;
+import org.onosproject.net.region.RegionService;
+import org.onosproject.rest.resources.CoreWebApplication;
+
+import javax.ws.rs.core.MediaType;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.Set;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Unit tests for region REST APIs.
+ */
+public class RegionsResourceTest extends ResourceTest {
+
+    final RegionService mockRegionService = createMock(RegionService.class);
+    final RegionAdminService mockRegionAdminService = createMock(RegionAdminService.class);
+
+    final RegionId regionId1 = RegionId.regionId("1");
+    final RegionId regionId2 = RegionId.regionId("2");
+    final RegionId regionId3 = RegionId.regionId("3");
+
+    final MockRegion region1 = new MockRegion(regionId1, "r1", Region.Type.RACK);
+    final MockRegion region2 = new MockRegion(regionId2, "r2", Region.Type.ROOM);
+    final MockRegion region3 = new MockRegion(regionId3, "r3", Region.Type.CAMPUS);
+
+    public RegionsResourceTest() {
+        super(CoreWebApplication.class);
+    }
+
+    /**
+     * Mock class for a region.
+     */
+    private static class MockRegion implements Region {
+
+        private final RegionId id;
+        private final String name;
+        private final Type type;
+        private final List<Set<NodeId>> masters;
+
+        public MockRegion(RegionId id, String name, Type type) {
+            this.id = id;
+            this.name = name;
+            this.type = type;
+
+            final NodeId nodeId1 = NodeId.nodeId("1");
+            final NodeId nodeId2 = NodeId.nodeId("2");
+            final NodeId nodeId3 = NodeId.nodeId("3");
+            final NodeId nodeId4 = NodeId.nodeId("4");
+
+            Set<NodeId> nodeIds1 = ImmutableSet.of(nodeId1);
+            Set<NodeId> nodeIds2 = ImmutableSet.of(nodeId1, nodeId2);
+            Set<NodeId> nodeIds3 = ImmutableSet.of(nodeId1, nodeId2, nodeId3);
+            Set<NodeId> nodeIds4 = ImmutableSet.of(nodeId1, nodeId2, nodeId3, nodeId4);
+
+            this.masters = ImmutableList.of(nodeIds1, nodeIds2, nodeIds3, nodeIds4);
+        }
+
+        @Override
+        public RegionId id() {
+            return this.id;
+        }
+
+        @Override
+        public String name() {
+            return this.name;
+        }
+
+        @Override
+        public Type type() {
+            return this.type;
+        }
+
+        @Override
+        public List<Set<NodeId>> masters() {
+            return this.masters;
+        }
+    }
+
+    /**
+     * Sets up the global values for all the tests.
+     */
+    @Before
+    public void setupTest() {
+        final CodecManager codecService = new CodecManager();
+        codecService.activate();
+        ServiceDirectory testDirectory =
+                new TestServiceDirectory()
+                .add(RegionService.class, mockRegionService)
+                .add(RegionAdminService.class, mockRegionAdminService)
+                .add(CodecService.class, codecService);
+        BaseResource.setServiceDirectory(testDirectory);
+    }
+
+    /**
+     * Hamcrest matcher to check that a meter representation in JSON matches
+     * the actual meter.
+     */
+    public static class RegionJsonMatcher extends TypeSafeMatcher<JsonObject> {
+        private final Region region;
+        private String reason = "";
+
+        public RegionJsonMatcher(Region regionValue) {
+            this.region = regionValue;
+        }
+
+        @Override
+        protected boolean matchesSafely(JsonObject jsonRegion) {
+
+            // check id
+            String jsonRegionId = jsonRegion.get("id").asString();
+            String regionId = region.id().toString();
+            if (!jsonRegionId.equals(regionId)) {
+                reason = "region id was " + jsonRegionId;
+                return false;
+            }
+
+            // check type
+            String jsonType = jsonRegion.get("type").asString();
+            String type = region.type().toString();
+            if (!jsonType.equals(type)) {
+                reason = "type was " + jsonType;
+                return false;
+            }
+
+            // check name
+            String jsonName = jsonRegion.get("name").asString();
+            String name = region.name();
+            if (!jsonName.equals(name)) {
+                reason = "name was " + jsonName;
+                return false;
+            }
+
+            // check size of master array
+            JsonArray jsonMasters = jsonRegion.get("masters").asArray();
+            if (jsonMasters.size() != region.masters().size()) {
+                reason = "masters size was " + jsonMasters.size();
+                return false;
+            }
+
+            // check master
+            for (Set<NodeId> set : region.masters()) {
+                boolean masterFound = false;
+                for (int masterIndex = 0; masterIndex < jsonMasters.size(); masterIndex++) {
+                    masterFound = checkEquality(jsonMasters.get(masterIndex).asArray(), set);
+                }
+
+                if (!masterFound) {
+                    reason = "master not found " + set.toString();
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText(reason);
+        }
+
+        private Set<NodeId> jsonToSet(JsonArray nodes) {
+            final Set<NodeId> nodeIds = Sets.newHashSet();
+            nodes.forEach(node -> nodeIds.add(NodeId.nodeId(node.asString())));
+            return nodeIds;
+        }
+
+        private boolean checkEquality(JsonArray nodes, Set<NodeId> nodeIds) {
+            Set<NodeId> jsonSet = jsonToSet(nodes);
+            if (jsonSet.size() == nodes.size()) {
+                return jsonSet.containsAll(nodeIds);
+            }
+            return false;
+        }
+    }
+
+    private static RegionJsonMatcher matchesRegion(Region region) {
+        return new RegionJsonMatcher(region);
+    }
+
+    /**
+     * Hamcrest matcher to check that a region is represented properly in a JSON
+     * array of regions.
+     */
+    public static class RegionJsonArrayMatcher extends TypeSafeMatcher<JsonArray> {
+        private final Region region;
+        private String reason = "";
+
+        public RegionJsonArrayMatcher(Region regionValue) {
+            this.region = regionValue;
+        }
+
+        @Override
+        protected boolean matchesSafely(JsonArray json) {
+            boolean regionFound = false;
+            for (int jsonRegionIndex = 0; jsonRegionIndex < json.size(); jsonRegionIndex++) {
+                final JsonObject jsonRegion = json.get(jsonRegionIndex).asObject();
+
+                final String regionId = region.id().toString();
+                final String jsonRegionId = jsonRegion.get("id").asString();
+                if (jsonRegionId.equals(regionId)) {
+                    regionFound = true;
+                    assertThat(jsonRegion, matchesRegion(region));
+                }
+            }
+
+            if (!regionFound) {
+                reason = "Region with id " + region.id().toString() + " not found";
+                return false;
+            } else {
+                return true;
+            }
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText(reason);
+        }
+    }
+
+    /**
+     * Factory to allocate a region array matcher.
+     *
+     * @param region region object we are looking for
+     * @return matcher
+     */
+    private static RegionJsonArrayMatcher hasRegion(Region region) {
+        return new RegionJsonArrayMatcher(region);
+    }
+
+    @Test
+    public void testRegionEmptyArray() {
+        expect(mockRegionService.getRegions()).andReturn(ImmutableSet.of()).anyTimes();
+        replay((mockRegionService));
+        final WebResource rs = resource();
+        final String response = rs.path("regions").get(String.class);
+        assertThat(response, is("{\"regions\":[]}"));
+
+        verify(mockRegionService);
+    }
+
+    /**
+     * Tests the results of the REST API GET when there are active regions.
+     */
+    @Test
+    public void testRegionsPopulatedArray() {
+        final Set<Region> regions = ImmutableSet.of(region1, region2, region3);
+        expect(mockRegionService.getRegions()).andReturn(regions).anyTimes();
+        replay(mockRegionService);
+
+        final WebResource rs = resource();
+        final String response = rs.path("regions").get(String.class);
+        final JsonObject result = Json.parse(response).asObject();
+        assertThat(result, notNullValue());
+
+        assertThat(result.names(), hasSize(1));
+        assertThat(result.names().get(0), is("regions"));
+        final JsonArray jsonRegions = result.get("regions").asArray();
+        assertThat(jsonRegions, notNullValue());
+        assertThat(jsonRegions, hasRegion(region1));
+        assertThat(jsonRegions, hasRegion(region2));
+        assertThat(jsonRegions, hasRegion(region3));
+
+        verify(mockRegionService);
+
+    }
+
+    /**
+     * Tests the result of a REST API GET for a region with region id.
+     */
+    @Test
+    public void testGetRegionById() {
+        expect(mockRegionService.getRegion(anyObject())).andReturn(region1).anyTimes();
+        replay(mockRegionService);
+
+        final WebResource rs = resource();
+        final String response = rs.path("regions/" + regionId1.toString()).get(String.class);
+        final JsonObject result = Json.parse(response).asObject();
+        assertThat(result, notNullValue());
+        assertThat(result, matchesRegion(region1));
+
+        verify(mockRegionService);
+    }
+
+    /**
+     * Tests creating a region with POST.
+     */
+    @Test
+    public void testRegionPost() {
+        mockRegionAdminService.createRegion(anyObject(), anyObject(),
+                anyObject(), anyObject());
+        expectLastCall().andReturn(region2).anyTimes();
+        replay(mockRegionAdminService);
+
+        WebResource rs = resource();
+        InputStream jsonStream = MetersResourceTest.class
+                .getResourceAsStream("post-region.json");
+
+        ClientResponse response = rs.path("regions")
+                .type(MediaType.APPLICATION_JSON_TYPE)
+                .post(ClientResponse.class, jsonStream);
+        assertThat(response.getStatus(), is(HttpURLConnection.HTTP_CREATED));
+
+        verify(mockRegionAdminService);
+    }
+
+    /**
+     * Tests updating a region with PUT.
+     */
+    @Test
+    public void testRegionPut() {
+        mockRegionAdminService.updateRegion(anyObject(), anyObject(),
+                anyObject(), anyObject());
+        expectLastCall().andReturn(region1).anyTimes();
+        replay(mockRegionAdminService);
+
+        WebResource rs = resource();
+        InputStream jsonStream = MetersResourceTest.class
+                .getResourceAsStream("post-region.json");
+
+        ClientResponse response = rs.path("regions/" + region1.id().toString())
+                .type(MediaType.APPLICATION_JSON_TYPE)
+                .put(ClientResponse.class, jsonStream);
+        assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
+
+        verify(mockRegionAdminService);
+    }
+
+    /**
+     * Tests deleting a region with DELETE.
+     */
+    @Test
+    public void testRegionDelete() {
+        mockRegionAdminService.removeRegion(anyObject());
+        expectLastCall();
+        replay(mockRegionAdminService);
+
+        WebResource rs = resource();
+        ClientResponse response = rs.path("regions/" + region1.id().toString())
+                .delete(ClientResponse.class);
+        assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
+
+        verify(mockRegionAdminService);
+    }
+
+    /**
+     * Tests retrieving device ids that are associated with the given region.
+     */
+    @Test
+    public void testGetRegionDevices() {
+        final DeviceId deviceId1 = DeviceId.deviceId("1");
+        final DeviceId deviceId2 = DeviceId.deviceId("2");
+        final DeviceId deviceId3 = DeviceId.deviceId("3");
+
+        final Set<DeviceId> deviceIds = ImmutableSet.of(deviceId1, deviceId2, deviceId3);
+
+        expect(mockRegionService.getRegionDevices(anyObject()))
+                .andReturn(deviceIds).anyTimes();
+        replay(mockRegionService);
+
+        final WebResource rs = resource();
+        final String response = rs.path("regions/" +
+                region1.id().toString() + "/devices").get(String.class);
+        final JsonObject result = Json.parse(response).asObject();
+        assertThat(result, notNullValue());
+
+        assertThat(result.names(), hasSize(1));
+        assertThat(result.names().get(0), is("deviceIds"));
+        final JsonArray jsonDeviceIds = result.get("deviceIds").asArray();
+        assertThat(jsonDeviceIds.size(), is(3));
+        assertThat(jsonDeviceIds.get(0).asString(), is("1"));
+        assertThat(jsonDeviceIds.get(1).asString(), is("2"));
+        assertThat(jsonDeviceIds.get(2).asString(), is("3"));
+
+        verify(mockRegionService);
+    }
+
+    /**
+     * Tests adding a set of devices in region with POST.
+     */
+    @Test
+    public void testAddDevicesPost() {
+        mockRegionAdminService.addDevices(anyObject(), anyObject());
+        expectLastCall();
+        replay(mockRegionAdminService);
+
+        WebResource rs = resource();
+        InputStream jsonStream = MetersResourceTest.class
+                .getResourceAsStream("region-deviceIds.json");
+
+        ClientResponse response = rs.path("regions/" +
+                region1.id().toString() + "/devices")
+                .type(MediaType.APPLICATION_JSON_TYPE)
+                .post(ClientResponse.class, jsonStream);
+        assertThat(response.getStatus(), is(HttpURLConnection.HTTP_CREATED));
+
+        verify(mockRegionAdminService);
+    }
+
+    /**
+     * Tests deleting a set of devices contained in the given region with DELETE.
+     */
+    @Test
+    public void testRemoveDevicesDelete() {
+        mockRegionAdminService.removeDevices(anyObject(), anyObject());
+        expectLastCall();
+        replay(mockRegionAdminService);
+
+        WebResource rs = resource();
+        InputStream jsonStream = MetersResourceTest.class
+                .getResourceAsStream("region-deviceIds.json");
+
+        ClientResponse response = rs.path("regions/" +
+                region1.id().toString() + "/devices")
+                .type(MediaType.APPLICATION_JSON_TYPE)
+                .delete(ClientResponse.class, jsonStream);
+        assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK));
+
+        verify(mockRegionAdminService);
+    }
+}
diff --git a/web/api/src/test/resources/org/onosproject/rest/post-region.json b/web/api/src/test/resources/org/onosproject/rest/post-region.json
new file mode 100644
index 0000000..087f66e
--- /dev/null
+++ b/web/api/src/test/resources/org/onosproject/rest/post-region.json
@@ -0,0 +1,13 @@
+{
+  "id": 1,
+  "type": "ROOM",
+  "name": "foo",
+  "masters": [
+    [
+      "1"
+    ],
+    [
+      "1", "2"
+    ]
+  ]
+}
\ No newline at end of file
diff --git a/web/api/src/test/resources/org/onosproject/rest/region-deviceIds.json b/web/api/src/test/resources/org/onosproject/rest/region-deviceIds.json
new file mode 100644
index 0000000..8445728
--- /dev/null
+++ b/web/api/src/test/resources/org/onosproject/rest/region-deviceIds.json
@@ -0,0 +1,7 @@
+{
+  "deviceIds": [
+    "of:0000000000000001",
+    "of:0000000000000002",
+    "of:0000000000000003"
+  ]
+}
\ No newline at end of file