Add JSON decoders for Host and HostLocation

- HostsWebResource will use the Host codec in parsing JSON for create-host requests

Change-Id: If51bf3433a4ab45889a94a6d11bbd3db6b96d074
(cherry picked from commit 46d2462e4e49855b0d533035250776589fd05d88)
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/HostCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/HostCodec.java
index d2890a4..dc6538e 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/HostCodec.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/HostCodec.java
@@ -15,57 +15,136 @@
  */
 package org.onosproject.codec.impl;
 
-import org.onlab.packet.IpAddress;
-import org.onosproject.codec.CodecContext;
-import org.onosproject.codec.JsonCodec;
-import org.onosproject.net.Host;
-import org.onosproject.net.HostLocation;
-
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.packet.EthType;
+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.onosproject.net.Annotations;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.provider.ProviderId;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
 
 /**
- * Host JSON codec.
+ * JSON codec for Host class.
  */
 public final class HostCodec extends AnnotatedCodec<Host> {
 
+    // JSON field names
+    public static final String HOST_ID = "id";
+    public static final String MAC = "mac";
+    public static final String VLAN = "vlan";
+    public static final String INNER_VLAN = "innerVlan";
+    public static final String OUTER_TPID = "outerTpid";
+    public static final String IS_CONFIGURED = "configured";
+    public static final String IS_SUSPENDED = "suspended";
+    public static final String IP_ADDRESSES = "ipAddresses";
+    public static final String HOST_LOCATIONS = "locations";
+    public static final String AUX_LOCATIONS = "auxLocations";
+
+    private static final String NULL_OBJECT_MSG = "Host cannot be null";
+    private static final String MISSING_MEMBER_MESSAGE = " member is required in Host";
+
     @Override
     public ObjectNode encode(Host host, CodecContext context) {
-        checkNotNull(host, "Host cannot be null");
+        checkNotNull(host, NULL_OBJECT_MSG);
+
         final JsonCodec<HostLocation> locationCodec =
                 context.codec(HostLocation.class);
+        // keep fields in string for compatibility
         final ObjectNode result = context.mapper().createObjectNode()
-                .put("id", host.id().toString())
-                .put("mac", host.mac().toString())
-                .put("vlan", host.vlan().toString())
-                .put("innerVlan", host.innerVlan().toString())
-                .put("outerTpid", host.tpid().toString())
-                .put("configured", host.configured());
+                .put(HOST_ID, host.id().toString())
+                .put(MAC, host.mac().toString())
+                .put(VLAN, host.vlan().toString())
+                .put(INNER_VLAN, host.innerVlan().toString())
+                // use a 4-digit hex string in coding an ethernet type
+                .put(OUTER_TPID, String.format("0x%04x", host.tpid().toShort()))
+                .put(IS_CONFIGURED, host.configured())
+                .put(IS_SUSPENDED, host.suspended());
 
-        final ArrayNode jsonIpAddresses = result.putArray("ipAddresses");
+        final ArrayNode jsonIpAddresses = result.putArray(IP_ADDRESSES);
         for (final IpAddress ipAddress : host.ipAddresses()) {
             jsonIpAddresses.add(ipAddress.toString());
         }
-        result.set("ipAddresses", jsonIpAddresses);
+        result.set(IP_ADDRESSES, jsonIpAddresses);
 
-        final ArrayNode jsonLocations = result.putArray("locations");
+        final ArrayNode jsonLocations = result.putArray(HOST_LOCATIONS);
         for (final HostLocation location : host.locations()) {
             jsonLocations.add(locationCodec.encode(location, context));
         }
-        result.set("locations", jsonLocations);
+        result.set(HOST_LOCATIONS, jsonLocations);
 
         if (host.auxLocations() != null) {
-            final ArrayNode jsonAuxLocations = result.putArray("auxLocations");
+            final ArrayNode jsonAuxLocations = result.putArray(AUX_LOCATIONS);
             for (final HostLocation auxLocation : host.auxLocations()) {
                 jsonAuxLocations.add(locationCodec.encode(auxLocation, context));
             }
-            result.set("auxLocations", jsonAuxLocations);
+            result.set(AUX_LOCATIONS, jsonAuxLocations);
         }
 
         return annotate(result, host, context);
     }
 
-}
+    @Override
+    public Host decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
 
+        MacAddress mac = MacAddress.valueOf(nullIsIllegal(
+                json.get(MAC), MAC + MISSING_MEMBER_MESSAGE).asText());
+        VlanId vlanId = VlanId.vlanId(nullIsIllegal(
+                json.get(VLAN), VLAN + MISSING_MEMBER_MESSAGE).asText());
+        HostId id = HostId.hostId(mac, vlanId);
+
+        ArrayNode locationNodes = nullIsIllegal(
+                (ArrayNode) json.get(HOST_LOCATIONS), HOST_LOCATIONS + MISSING_MEMBER_MESSAGE);
+        Set<HostLocation> hostLocations =
+                context.codec(HostLocation.class).decode(locationNodes, context)
+                .stream().collect(Collectors.toSet());
+
+        ArrayNode ipNodes = nullIsIllegal(
+                (ArrayNode) json.get(IP_ADDRESSES), IP_ADDRESSES + MISSING_MEMBER_MESSAGE);
+        Set<IpAddress> ips = new HashSet<>();
+        ipNodes.forEach(ipNode -> {
+            ips.add(IpAddress.valueOf(ipNode.asText()));
+        });
+
+        // check optional fields
+        JsonNode innerVlanIdNode = json.get(INNER_VLAN);
+        VlanId innerVlanId = (null == innerVlanIdNode) ? VlanId.NONE :
+                VlanId.vlanId(innerVlanIdNode.asText());
+        JsonNode outerTpidNode = json.get(OUTER_TPID);
+        EthType outerTpid = (null == outerTpidNode) ? EthType.EtherType.UNKNOWN.ethType() :
+                EthType.EtherType.lookup((short) (Integer.decode(outerTpidNode.asText()) & 0xFFFF)).ethType();
+        JsonNode configuredNode = json.get(IS_CONFIGURED);
+        boolean configured = (null == configuredNode) ? false : configuredNode.asBoolean();
+        JsonNode suspendedNode = json.get(IS_SUSPENDED);
+        boolean suspended = (null == suspendedNode) ? false : suspendedNode.asBoolean();
+
+        ArrayNode auxLocationNodes = (ArrayNode) json.get(AUX_LOCATIONS);
+        Set<HostLocation> auxHostLocations = (null == auxLocationNodes) ? null :
+                context.codec(HostLocation.class).decode(auxLocationNodes, context)
+                        .stream().collect(Collectors.toSet());
+
+        Annotations annotations = extractAnnotations(json, context);
+
+        return new DefaultHost(ProviderId.NONE, id, mac, vlanId,
+                               hostLocations, auxHostLocations, ips, innerVlanId,
+                               outerTpid, configured, suspended, annotations);
+    }
+
+}
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/HostLocationCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/HostLocationCodec.java
index efcd133..da4405d 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/HostLocationCodec.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/HostLocationCodec.java
@@ -17,23 +17,45 @@
 
 import org.onosproject.codec.CodecContext;
 import org.onosproject.codec.JsonCodec;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.HostLocation;
 
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.net.PortNumber;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
 
 /**
- * Host JSON codec.
+ * HostLocation JSON codec.
  */
 public final class HostLocationCodec extends JsonCodec<HostLocation> {
 
+    public static final String ELEMENT_ID = "elementId";
+    public static final String PORT = "port";
+
+    private static final String MISSING_MEMBER_MESSAGE =
+            " member is required in HostLocation";
+
     @Override
     public ObjectNode encode(HostLocation hostLocation, CodecContext context) {
         checkNotNull(hostLocation, "Host location cannot be null");
         return context.mapper().createObjectNode()
-                .put("elementId", hostLocation.elementId().toString())
-                .put("port", hostLocation.port().toString());
+                .put(ELEMENT_ID, hostLocation.elementId().toString())
+                .put(PORT, hostLocation.port().toString());
     }
 
+    @Override
+    public HostLocation decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        DeviceId deviceId = DeviceId.deviceId(nullIsIllegal(
+                json.get(ELEMENT_ID), ELEMENT_ID + MISSING_MEMBER_MESSAGE).asText());
+        PortNumber portNumber = PortNumber.portNumber(nullIsIllegal(
+                json.get(PORT), PORT + MISSING_MEMBER_MESSAGE).asText());
+
+        return new HostLocation(deviceId, portNumber, 0);
+    }
 }
diff --git a/core/common/src/test/java/org/onosproject/codec/impl/HostCodecTest.java b/core/common/src/test/java/org/onosproject/codec/impl/HostCodecTest.java
new file mode 100644
index 0000000..4edbc70
--- /dev/null
+++ b/core/common/src/test/java/org/onosproject/codec/impl/HostCodecTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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 org.junit.Test;
+import org.onlab.packet.EthType;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.net.Annotations;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.provider.ProviderId;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.onosproject.codec.impl.JsonCodecUtils.assertJsonEncodable;
+
+/**
+ * Unit test for HostCodec.
+ */
+public class HostCodecTest {
+
+    // Make sure this Host and the corresponding JSON file have the same values
+    private static final MacAddress MAC;
+    private static final VlanId VLAN_ID;
+    private static final HostId HOST_ID;
+    private static final DeviceId DEVICE_ID;
+    private static final PortNumber PORT_NUM;
+    private static final Set<HostLocation> HOST_LOCATIONS = new HashSet<>();
+    private static final PortNumber AUX_PORT_NUM;
+    private static final Set<HostLocation> AUX_HOST_LOCATIONS = new HashSet<>();
+    private static final Set<IpAddress> IPS;
+    private static final VlanId INNER_VLAN_ID;
+    private static final EthType OUTER_TPID;
+    private static final Annotations ANNOTATIONS;
+    private static final Host HOST;
+
+    private MockCodecContext context = new MockCodecContext();
+    private JsonCodec<Host> hostCodec = context.codec(Host.class);
+
+    private static final String JSON_FILE = "simple-host.json";
+
+    static {
+        // Make sure these members have same values with the corresponding JSON fields
+        MAC = MacAddress.valueOf("46:E4:3C:A4:17:C8");
+        VLAN_ID = VlanId.vlanId("None");
+        HOST_ID = HostId.hostId(MAC, VLAN_ID);
+        DEVICE_ID = DeviceId.deviceId("of:0000000000000002");
+        PORT_NUM = PortNumber.portNumber("3");
+        HOST_LOCATIONS.add(new HostLocation(DEVICE_ID, PORT_NUM, 0));
+        AUX_PORT_NUM = PortNumber.portNumber("4");
+        AUX_HOST_LOCATIONS.add(new HostLocation(DEVICE_ID, AUX_PORT_NUM, 0));
+        IPS = new HashSet<>();
+        IPS.add(IpAddress.valueOf("127.0.0.1"));
+        INNER_VLAN_ID = VlanId.vlanId("10");
+        OUTER_TPID = EthType.EtherType.lookup((short) (Integer.decode("0x88a8") & 0xFFFF)).ethType();
+        ANNOTATIONS = DefaultAnnotations.builder().set("key1", "val1").build();
+        HOST = new DefaultHost(ProviderId.NONE, HOST_ID, MAC, VLAN_ID,
+                               HOST_LOCATIONS, AUX_HOST_LOCATIONS, IPS, INNER_VLAN_ID,
+                               OUTER_TPID, false, false, ANNOTATIONS);
+    }
+
+    @Test
+    public void testCodec() {
+        assertNotNull(hostCodec);
+        assertJsonEncodable(context, hostCodec, HOST);
+    }
+
+    @Test
+    public void testDecode() throws IOException {
+        InputStream jsonStream = HostCodec.class.getResourceAsStream(JSON_FILE);
+        JsonNode jsonString = context.mapper().readTree(jsonStream);
+
+        Host expected = hostCodec.decode((ObjectNode) jsonString, context);
+        assertEquals(expected, HOST);
+    }
+
+    @Test
+    public void testEncode() throws IOException {
+        InputStream jsonStream = HostCodec.class.getResourceAsStream(JSON_FILE);
+        JsonNode jsonString = context.mapper().readTree(jsonStream);
+
+        ObjectNode expected = hostCodec.encode(HOST, context);
+        // Host ID is not a field in Host but rather derived from MAC + VLAN.
+        // Derived information should not be part of the JSON really.
+        // However, we keep it as is for backward compatibility.
+        expected.remove(HostCodec.HOST_ID);
+
+        assertEquals(expected, jsonString);
+    }
+
+}
\ No newline at end of file
diff --git a/core/common/src/test/resources/org/onosproject/codec/impl/simple-host.json b/core/common/src/test/resources/org/onosproject/codec/impl/simple-host.json
new file mode 100644
index 0000000..bd547e0
--- /dev/null
+++ b/core/common/src/test/resources/org/onosproject/codec/impl/simple-host.json
@@ -0,0 +1,26 @@
+{
+  "mac": "46:E4:3C:A4:17:C8",
+  "vlan": "None",
+  "ipAddresses": [
+    "127.0.0.1"
+  ],
+  "locations": [
+    {
+      "elementId": "of:0000000000000002",
+      "port": "3"
+    }
+   ],
+  "auxLocations": [
+    {
+      "elementId": "of:0000000000000002",
+      "port": "4"
+    }
+  ],
+  "innerVlan": "10",
+  "outerTpid": "0x88a8",
+  "configured": false,
+  "suspended": false,
+  "annotations": {
+    "key1": "val1"
+  }
+}
diff --git a/web/api/src/main/java/org/onosproject/rest/resources/HostsWebResource.java b/web/api/src/main/java/org/onosproject/rest/resources/HostsWebResource.java
index 6bdd483..1dcbb5d 100644
--- a/web/api/src/main/java/org/onosproject/rest/resources/HostsWebResource.java
+++ b/web/api/src/main/java/org/onosproject/rest/resources/HostsWebResource.java
@@ -16,17 +16,9 @@
 package org.onosproject.rest.resources;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import org.onlab.packet.EthType;
-import org.onlab.packet.IpAddress;
-import org.onlab.packet.MacAddress;
-import org.onlab.packet.VlanId;
-import org.onosproject.net.ConnectPoint;
-import org.onosproject.net.DefaultAnnotations;
 import org.onosproject.net.Host;
 import org.onosproject.net.HostId;
-import org.onosproject.net.HostLocation;
 import org.onosproject.net.SparseAnnotations;
 import org.onosproject.net.host.DefaultHostDescription;
 import org.onosproject.net.host.HostAdminService;
@@ -52,10 +44,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
 
 import static org.onlab.util.Tools.nullIsNotFound;
 import static org.onlab.util.Tools.readTreeFromStream;
@@ -70,8 +58,6 @@
     @Context
     private UriInfo uriInfo;
     private static final String HOST_NOT_FOUND = "Host is not found";
-    private static final String[] REMOVAL_KEYS = {"mac", "vlan", "locations", "ipAddresses",
-            "auxLocations", "innerVlan", "outerTpid"};
 
     /**
      * Get all end-station hosts.
@@ -221,107 +207,16 @@
          * @return host ID of new host created
          */
         private HostId parseHost(JsonNode node) {
-            MacAddress mac = MacAddress.valueOf(node.get("mac").asText());
-            VlanId vlanId = VlanId.vlanId((short) node.get("vlan").asInt(VlanId.UNTAGGED));
+            Host host = codec(Host.class).decode((ObjectNode) node, HostsWebResource.this);
 
-            // Parse locations
-            if (null == node.get("locations")) {
-                throw new IllegalArgumentException("location isn't specified");
-            }
-            Iterator<JsonNode> locationNodes = node.get("locations").elements();
-            Set<HostLocation> locations = new HashSet<>();
-            while (locationNodes.hasNext()) {
-                JsonNode locationNode = locationNodes.next();
-                String deviceAndPort = locationNode.get("elementId").asText() + "/" +
-                        locationNode.get("port").asText();
-                HostLocation hostLocation = new HostLocation(ConnectPoint.deviceConnectPoint(deviceAndPort), 0);
-                locations.add(hostLocation);
-            }
-
-            // Parse ipAddresses
-            if (null == node.get("ipAddresses")) {
-                throw new IllegalArgumentException("ipAddress isn't specified");
-            }
-            Iterator<JsonNode> ipNodes = node.get("ipAddresses").elements();
-            Set<IpAddress> ips = new HashSet<>();
-            while (ipNodes.hasNext()) {
-                ips.add(IpAddress.valueOf(ipNodes.next().asText()));
-            }
-
-            // Parse auxLocations
-            Set<HostLocation> auxLocations;
-            JsonNode auxLocationsNode = node.get("auxLocations");
-            if (null == auxLocationsNode) {
-                auxLocations = null;
-            } else {
-                Iterator<JsonNode> auxLocationNodes = auxLocationsNode.elements();
-                auxLocations = new HashSet<>();
-                while (auxLocationNodes.hasNext()) {
-                    JsonNode auxLocationNode = auxLocationNodes.next();
-                    String deviceAndPort = auxLocationNode.get("elementId").asText() + "/" +
-                            auxLocationNode.get("port").asText();
-                    HostLocation auxLocation = new HostLocation(ConnectPoint.deviceConnectPoint(deviceAndPort), 0);
-                    auxLocations.add(auxLocation);
-                }
-            }
-
-            // Parse innerVlan
-            JsonNode innerVlanNode = node.get("innerVlan");
-            VlanId innerVlan = (null == innerVlanNode) ? VlanId.NONE : VlanId.vlanId(innerVlanNode.asText());
-
-            // Parse outerTpid
-            JsonNode outerTpidNode = node.get("outerTpid");
-            EthType outerTpid = (null == outerTpidNode) ? EthType.EtherType.UNKNOWN.ethType() :
-                    EthType.EtherType.lookup((short) (Integer.decode(outerTpidNode.asText()) & 0xFFFF)).ethType();
-
-            // try to remove elements from json node after reading them
-            SparseAnnotations annotations = annotations(removeElements(node, REMOVAL_KEYS));
-            // Update host inventory
-
-            HostId hostId = HostId.hostId(mac, vlanId);
-            DefaultHostDescription desc = new DefaultHostDescription(mac, vlanId, locations, auxLocations,
-                    ips, innerVlan, outerTpid, true, annotations);
+            HostId hostId = host.id();
+            DefaultHostDescription desc = new DefaultHostDescription(
+                    host.mac(), host.vlan(), host.locations(), host.ipAddresses(), host.innerVlan(),
+                    host.tpid(), host.configured(), (SparseAnnotations) host.annotations());
             hostProviderService.hostDetected(hostId, desc, false);
+
             return hostId;
         }
-
-        /**
-         * Remove a set of elements from JsonNode by specifying keys.
-         *
-         * @param node JsonNode containing host information
-         * @param removalKeys key of elements that need to be removed
-         * @return removal keys
-         */
-        private JsonNode removeElements(JsonNode node, String[] removalKeys) {
-            ObjectMapper mapper = new ObjectMapper();
-            Map<String, Object> map = mapper.convertValue(node, Map.class);
-            for (String key : removalKeys) {
-                map.remove(key);
-            }
-            return mapper.convertValue(map, JsonNode.class);
-        }
-
-        /**
-         * Produces annotations from specified JsonNode. Copied from the ConfigProvider
-         * class for use in the POST method.
-         *
-         * @param node node to be annotated
-         * @return SparseAnnotations object with information about node
-         */
-        private SparseAnnotations annotations(JsonNode node) {
-            if (node == null) {
-                return DefaultAnnotations.EMPTY;
-            }
-
-            DefaultAnnotations.Builder builder = DefaultAnnotations.builder();
-            Iterator<String> it = node.fieldNames();
-            while (it.hasNext()) {
-                String k = it.next();
-                builder.set(k, node.get(k).asText());
-            }
-            return builder.build();
-        }
-
     }
 }
 
diff --git a/web/api/src/test/java/org/onosproject/rest/resources/HostResourceTest.java b/web/api/src/test/java/org/onosproject/rest/resources/HostResourceTest.java
index 808fb42..70c82fd 100644
--- a/web/api/src/test/java/org/onosproject/rest/resources/HostResourceTest.java
+++ b/web/api/src/test/java/org/onosproject/rest/resources/HostResourceTest.java
@@ -236,7 +236,7 @@
         @Override
         public boolean matchesSafely(JsonArray json) {
             boolean hostFound = false;
-            final int expectedAttributes = 8;
+            final int expectedAttributes = 9;
             for (int jsonHostIndex = 0; jsonHostIndex < json.size();
                  jsonHostIndex++) {