CORD-176 Added web resources and codecs for service network and port

Change-Id: I15db1036fa9ee4041520abbcc36bae022ff03a1c
diff --git a/src/main/java/org/opencord/cordvtn/codec/CodecRegistrator.java b/src/main/java/org/opencord/cordvtn/codec/CodecRegistrator.java
new file mode 100644
index 0000000..0352f98
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/codec/CodecRegistrator.java
@@ -0,0 +1,53 @@
+/*
+ * 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.opencord.cordvtn.codec;
+
+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.onosproject.codec.CodecService;
+import org.opencord.cordvtn.api.ServiceNetwork;
+import org.opencord.cordvtn.api.ServicePort;
+import org.slf4j.Logger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of the JSON codec brokering service for VTN.
+ */
+@Component(immediate = true)
+public class CodecRegistrator {
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CodecService codecService;
+
+    @Activate
+    public void activate() {
+        codecService.registerCodec(ServiceNetwork.class, new ServiceNetworkCodec());
+        codecService.registerCodec(ServicePort.class, new ServicePortCodec());
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        codecService.unregisterCodec(ServiceNetwork.class);
+        codecService.unregisterCodec(ServicePort.class);
+        log.info("Stopped");
+    }
+}
diff --git a/src/main/java/org/opencord/cordvtn/codec/ServiceNetworkCodec.java b/src/main/java/org/opencord/cordvtn/codec/ServiceNetworkCodec.java
new file mode 100644
index 0000000..52f9a7e
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/codec/ServiceNetworkCodec.java
@@ -0,0 +1,80 @@
+/*
+ * 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.opencord.cordvtn.codec;
+
+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.opencord.cordvtn.api.NetworkId;
+import org.opencord.cordvtn.api.ServiceNetwork;
+import org.opencord.cordvtn.api.ServiceNetwork.DirectAccessType;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static org.opencord.cordvtn.api.ServiceNetwork.DirectAccessType.BIDIRECTIONAL;
+import static org.opencord.cordvtn.api.ServiceNetwork.DirectAccessType.UNIDIRECTIONAL;
+import static org.opencord.cordvtn.api.ServiceNetwork.ServiceNetworkType.valueOf;
+
+/**
+ * Service network JSON codec.
+ */
+public final class ServiceNetworkCodec extends JsonCodec<ServiceNetwork> {
+
+    private static final String ID = "id";
+    private static final String TYPE = "type";
+    private static final String PROVIDER_NETWORKS = "providerNetworks";
+    private static final String BIDIRECT = "bidirectional";
+
+    @Override
+    public ObjectNode encode(ServiceNetwork snet, CodecContext context) {
+        ObjectNode result = context.mapper().createObjectNode()
+                .put(ID, snet.id().id())
+                .put(TYPE, snet.type().name().toLowerCase());
+
+        ArrayNode providers = context.mapper().createArrayNode();
+        snet.providers().entrySet().forEach(provider -> {
+            ObjectNode providerJson = context.mapper().createObjectNode()
+                    .put(ID, provider.getKey().id())
+                    .put(BIDIRECT, provider.getValue() == BIDIRECTIONAL ? TRUE : FALSE);
+            providers.add(providerJson);
+        });
+
+        result.set(PROVIDER_NETWORKS, providers);
+        return result;
+    }
+
+    @Override
+    public ServiceNetwork decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        ServiceNetwork.Builder snetBuilder = ServiceNetwork.builder()
+                .id(NetworkId.of(json.get(ID).asText()))
+                .type(valueOf(json.get(TYPE).asText().toUpperCase()));
+
+        if (json.get(PROVIDER_NETWORKS) != null) {
+            json.get(PROVIDER_NETWORKS).forEach(provider -> {
+                NetworkId providerId = NetworkId.of(provider.get(ID).asText());
+                DirectAccessType type = provider.get(BIDIRECT).asBoolean() ?
+                        BIDIRECTIONAL : UNIDIRECTIONAL;
+                snetBuilder.addProvider(providerId, type);
+            });
+        }
+        return snetBuilder.build();
+    }
+}
diff --git a/src/main/java/org/opencord/cordvtn/codec/ServicePortCodec.java b/src/main/java/org/opencord/cordvtn/codec/ServicePortCodec.java
new file mode 100644
index 0000000..9c4343f
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/codec/ServicePortCodec.java
@@ -0,0 +1,82 @@
+/*
+ * 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.opencord.cordvtn.codec;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.opencord.cordvtn.api.AddressPair;
+import org.opencord.cordvtn.api.PortId;
+import org.opencord.cordvtn.api.ServicePort;
+
+/**
+ * Service port JSON codec.
+ */
+public final class ServicePortCodec extends JsonCodec<ServicePort> {
+
+    private static final String ID = "id";
+    private static final String VLAN_ID = "vlan_id";
+    private static final String FLOATING_ADDRESS_PAIRS = "floating_address_pairs";
+    private static final String IP_ADDRESS = "ip_address";
+    private static final String MAC_ADDRESS = "mac_address";
+
+    @Override
+    public ObjectNode encode(ServicePort sport, CodecContext context) {
+        ObjectNode result = context.mapper().createObjectNode()
+                .put(ID, sport.id().id());
+        if (sport.vlanId().isPresent()) {
+            result.put(VLAN_ID, sport.vlanId().get().id());
+        }
+
+        ArrayNode addressPairs = context.mapper().createArrayNode();
+        sport.addressPairs().forEach(pair -> {
+            ObjectNode pairJson = context.mapper().createObjectNode()
+                    .put(IP_ADDRESS, pair.ip().toString())
+                    .put(MAC_ADDRESS, pair.mac().toString());
+            addressPairs.add(pairJson);
+        });
+        result.set(FLOATING_ADDRESS_PAIRS, addressPairs);
+        return result;
+    }
+
+    @Override
+    public ServicePort decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        ServicePort.Builder sportBuilder = ServicePort.builder()
+                .id(PortId.of(json.get(ID).asText()));
+
+        if (json.get(VLAN_ID) != null) {
+            sportBuilder.vlanId(VlanId.vlanId(json.get(VLAN_ID).asText()));
+        }
+
+        if (json.get(FLOATING_ADDRESS_PAIRS) != null) {
+            json.get(FLOATING_ADDRESS_PAIRS).forEach(pair -> {
+                AddressPair addrPair = AddressPair.of(
+                        IpAddress.valueOf(pair.get(IP_ADDRESS).asText()),
+                        MacAddress.valueOf(pair.get(MAC_ADDRESS).asText()));
+                sportBuilder.addAddressPair(addrPair);
+            });
+        }
+        return sportBuilder.build();
+    }
+}
diff --git a/src/main/java/org/opencord/cordvtn/codec/package-info.java b/src/main/java/org/opencord/cordvtn/codec/package-info.java
new file mode 100644
index 0000000..1ac7b31
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/codec/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * JSON codecs for the web resources.
+ */
+package org.opencord.cordvtn.codec;
\ No newline at end of file
diff --git a/src/main/java/org/opencord/cordvtn/rest/CordVtnWebApplication.java b/src/main/java/org/opencord/cordvtn/rest/CordVtnWebApplication.java
index a9af9d2..fd26c63 100644
--- a/src/main/java/org/opencord/cordvtn/rest/CordVtnWebApplication.java
+++ b/src/main/java/org/opencord/cordvtn/rest/CordVtnWebApplication.java
@@ -26,7 +26,9 @@
 public class CordVtnWebApplication extends AbstractWebApplication {
     @Override
     public Set<Class<?>> getClasses() {
-        return getClasses(ServiceDependencyWebResource.class,
+        return getClasses(ServiceNetworkWebResource.class,
+                          ServicePortWebResource.class,
+                          ServiceDependencyWebResource.class,
                           NeutronMl2NetworksWebResource.class,
                           NeutronMl2SubnetsWebResource.class,
                           NeutronMl2PortsWebResource.class);
diff --git a/src/main/java/org/opencord/cordvtn/rest/ServiceNetworkWebResource.java b/src/main/java/org/opencord/cordvtn/rest/ServiceNetworkWebResource.java
new file mode 100644
index 0000000..fbd6ddd
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/rest/ServiceNetworkWebResource.java
@@ -0,0 +1,187 @@
+/*
+ * 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.opencord.cordvtn.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onosproject.rest.AbstractWebResource;
+import org.opencord.cordvtn.api.CordVtnStore;
+import org.opencord.cordvtn.api.NetworkId;
+import org.opencord.cordvtn.api.ServiceNetwork;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.status;
+
+/**
+ * Query and manage service networks.
+ */
+@Path("serviceNetworks")
+public class ServiceNetworkWebResource extends AbstractWebResource {
+    protected final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final String MESSAGE = "Received service network ";
+    private static final String SERVICE_NETWORK = "ServiceNetwork";
+    private static final String SERVICE_NETWORKS = "ServiceNetworks";
+
+    private final CordVtnStore service = DefaultServiceDirectory.getService(CordVtnStore.class);
+
+    @Context
+    private UriInfo uriInfo;
+
+    /**
+     * Creates a service network from the JSON input stream.
+     *
+     * @param input service network JSON stream
+     * @return 201 CREATED if the JSON is correct, 400 BAD_REQUEST if the JSON
+     * is invalid or duplicated network with different properties exists
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response createServiceNetwork(InputStream input) {
+        try {
+            JsonNode jsonTree = mapper().enable(INDENT_OUTPUT).readTree(input);
+            log.trace(MESSAGE + "CREATE " + mapper().writeValueAsString(jsonTree));
+
+            ObjectNode snetJson = (ObjectNode) jsonTree.get(SERVICE_NETWORK);
+            if (snetJson == null) {
+                throw new IllegalArgumentException();
+            }
+
+            final ServiceNetwork snet = codec(ServiceNetwork.class).decode(snetJson, this);
+            service.createServiceNetwork(snet);
+
+            UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                    .path(SERVICE_NETWORKS)
+                    .path(snet.id().id());
+
+            return created(locationBuilder.build()).build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Updates the service network with the specified identifier.
+     *
+     * @param id network identifier
+     * @return 200 OK with a service network, 400 BAD_REQUEST if the requested
+     * network does not exist
+     */
+    @PUT
+    @Path("{id}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response updateServiceNetwork(@PathParam("id") String id, InputStream input) {
+        try {
+            JsonNode jsonTree = mapper().enable(INDENT_OUTPUT).readTree(input);
+            log.trace(MESSAGE + "UPDATE " + mapper().writeValueAsString(jsonTree));
+
+            ObjectNode snetJson = (ObjectNode) jsonTree.get(SERVICE_NETWORK);
+            if (snetJson == null) {
+                throw new IllegalArgumentException();
+            }
+
+            final ServiceNetwork snet = codec(ServiceNetwork.class).decode(snetJson, this);
+            service.updateServiceNetwork(snet);
+
+            ObjectNode result = this.mapper().createObjectNode();
+            result.set(SERVICE_NETWORK, codec(ServiceNetwork.class).encode(snet, this));
+            return ok(result).build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Returns all service networks.
+     *
+     * @return 200 OK with set of service networks
+     */
+    @GET
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getServiceNetworks() {
+        log.trace(MESSAGE + "GET");
+
+        Set<ServiceNetwork> snets = service.getServiceNetworks();
+        return ok(encodeArray(ServiceNetwork.class, SERVICE_NETWORKS, snets)).build();
+    }
+
+    /**
+     * Returns the service network with the specified identifier.
+     *
+     * @param id network identifier
+     * @return 200 OK with a service network, 404 NOT_FOUND if the requested
+     * network does not exist
+     */
+    @GET
+    @Path("{id}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getServiceNetwork(@PathParam("id") String id) {
+        log.trace(MESSAGE + "GET " + id);
+
+        ServiceNetwork snet = service.getServiceNetwork(NetworkId.of(id));
+        if (snet == null) {
+            return status(NOT_FOUND).build();
+        }
+
+        ObjectNode result = this.mapper().createObjectNode();
+        result.set(SERVICE_NETWORK, codec(ServiceNetwork.class).encode(snet, this));
+        return ok(result).build();
+    }
+
+    /**
+     * Removes the service network.
+     *
+     * @param id network identifier
+     * @return 204 NO CONTENT, 400 BAD_REQUEST if the network does not exist
+     */
+    @DELETE
+    @Path("{id}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response deleteServiceNetwork(@PathParam("id") String id) {
+        log.trace(MESSAGE + "DELETE " + id);
+
+        service.removeServiceNetwork(NetworkId.of(id));
+        return noContent().build();
+    }
+}
diff --git a/src/main/java/org/opencord/cordvtn/rest/ServicePortWebResource.java b/src/main/java/org/opencord/cordvtn/rest/ServicePortWebResource.java
new file mode 100644
index 0000000..7b18e10
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/rest/ServicePortWebResource.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2016-present Open Porting 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.opencord.cordvtn.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onosproject.rest.AbstractWebResource;
+import org.opencord.cordvtn.api.CordVtnStore;
+import org.opencord.cordvtn.api.PortId;
+import org.opencord.cordvtn.api.ServicePort;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.status;
+
+/**
+ * Query and manage service ports.
+ */
+@Path("servicePorts")
+public class ServicePortWebResource extends AbstractWebResource {
+    protected final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final String MESSAGE = "Received service port ";
+    private static final String SERVICE_PORT = "ServicePort";
+    private static final String SERVICE_PORTS = "ServicePorts";
+
+    private final CordVtnStore service = DefaultServiceDirectory.getService(CordVtnStore.class);
+
+    @Context
+    private UriInfo uriInfo;
+
+    /**
+     * Creates a service port from the JSON input stream.
+     *
+     * @param input service port JSON stream
+     * @return 201 CREATED if the JSON is correct, 400 BAD_REQUEST if the JSON
+     * is invalid or duplicated port with different properties exists
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response createServicePort(InputStream input) {
+        try {
+            JsonNode jsonTree = mapper().enable(INDENT_OUTPUT).readTree(input);
+            log.trace(MESSAGE + "CREATE " + mapper().writeValueAsString(jsonTree));
+
+            ObjectNode portJson = (ObjectNode) jsonTree.get(SERVICE_PORT);
+            if (portJson == null) {
+                throw new IllegalArgumentException();
+            }
+
+            final ServicePort sport = codec(ServicePort.class).decode(portJson, this);
+            service.createServicePort(sport);
+
+            UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                    .path(SERVICE_PORTS)
+                    .path(sport.id().id());
+
+            return created(locationBuilder.build()).build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Returns all service ports.
+     *
+     * @return 200 OK with set of service ports
+     */
+    @GET
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getServicePorts() {
+        log.debug(MESSAGE + "GET");
+
+        Set<ServicePort> sports = service.getServicePorts();
+        return ok(encodeArray(ServicePort.class, SERVICE_PORTS, sports)).build();
+    }
+
+    /**
+     * Returns the service port with the specified identifier.
+     *
+     * @param id port identifier
+     * @return 200 OK with a service port, 404 NOT_FOUND if the requested
+     * port does not exist
+     */
+    @GET
+    @Path("{id}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getServicePort(@PathParam("id") String id) {
+        log.debug(MESSAGE + "GET " + id);
+
+        ServicePort sport = service.getServicePort(PortId.of(id));
+        if (sport == null) {
+            return status(NOT_FOUND).build();
+        }
+
+        ObjectNode result = this.mapper().createObjectNode();
+        result.set(SERVICE_PORT, codec(ServicePort.class).encode(sport, this));
+        return ok(result).build();
+    }
+
+    /**
+     * Removes the service port.
+     *
+     * @param id port identifier
+     * @return 204 NO CONTENT, 400 BAD_REQUEST if the network does not exist
+     */
+    @DELETE
+    @Path("{id}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response deleteServicePort(@PathParam("id") String id) {
+        log.debug(MESSAGE + "DELETE " + id);
+
+        service.removeServicePort(PortId.of(id));
+        return noContent().build();
+    }
+}