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"
+  }
+}