[ONOS-6169] Implement codec for LispGcAddress with unit test

Change-Id: I50f2bf62629c0dd4c020eacb67a8232fb4ef182f
diff --git a/drivers/lisp/src/main/java/org/onosproject/drivers/lisp/extensions/LispMappingExtensionCodecRegistrator.java b/drivers/lisp/src/main/java/org/onosproject/drivers/lisp/extensions/LispMappingExtensionCodecRegistrator.java
index 3fa316f..d41fd0c 100644
--- a/drivers/lisp/src/main/java/org/onosproject/drivers/lisp/extensions/LispMappingExtensionCodecRegistrator.java
+++ b/drivers/lisp/src/main/java/org/onosproject/drivers/lisp/extensions/LispMappingExtensionCodecRegistrator.java
@@ -23,6 +23,7 @@
 import org.onosproject.codec.CodecService;
 import org.onosproject.drivers.lisp.extensions.codec.LispAppDataAddressCodec;
 import org.onosproject.drivers.lisp.extensions.codec.LispAsAddressCodec;
+import org.onosproject.drivers.lisp.extensions.codec.LispGcAddressCodec;
 import org.onosproject.mapping.web.MappingCodecRegistrator;
 import org.slf4j.Logger;
 
@@ -50,6 +51,7 @@
 
         codecService.registerCodec(LispAppDataAddress.class, new LispAppDataAddressCodec());
         codecService.registerCodec(LispAsAddress.class, new LispAsAddressCodec());
+        codecService.registerCodec(LispGcAddress.class, new LispGcAddressCodec());
 
         log.info("Started");
     }
@@ -58,6 +60,7 @@
     public void deactivate() {
         codecService.unregisterCodec(LispAppDataAddress.class);
         codecService.unregisterCodec(LispAsAddress.class);
+        codecService.unregisterCodec(LispGcAddress.class);
 
         registrator.deactivate();
         registrator = null;
diff --git a/drivers/lisp/src/main/java/org/onosproject/drivers/lisp/extensions/codec/LispGcAddressCodec.java b/drivers/lisp/src/main/java/org/onosproject/drivers/lisp/extensions/codec/LispGcAddressCodec.java
new file mode 100644
index 0000000..254ebe8
--- /dev/null
+++ b/drivers/lisp/src/main/java/org/onosproject/drivers/lisp/extensions/codec/LispGcAddressCodec.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2017-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.onosproject.drivers.lisp.extensions.codec;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.drivers.lisp.extensions.LispGcAddress;
+import org.onosproject.mapping.addresses.MappingAddress;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
+
+/**
+ * LISP geo coordinate address codec.
+ */
+public final class LispGcAddressCodec extends JsonCodec<LispGcAddress> {
+
+    protected static final String NORTH = "north";
+    protected static final String LATITUDE_DEGREE = "latitudeDegree";
+    protected static final String LATITUDE_MINUTE = "latitudeMinute";
+    protected static final String LATITUDE_SECOND = "latitudeSecond";
+    protected static final String EAST = "east";
+    protected static final String LONGITUDE_DEGREE = "longitudeDegree";
+    protected static final String LONGITUDE_MINUTE = "longitudeMinute";
+    protected static final String LONGITUDE_SECOND = "longitudeSecond";
+    protected static final String ALTITUDE = "altitude";
+    protected static final String ADDRESS = "address";
+
+    private static final String MISSING_MEMBER_MESSAGE =
+                                " member is required in LispGcAddress";
+
+    @Override
+    public ObjectNode encode(LispGcAddress address, CodecContext context) {
+        checkNotNull(address, "LispGcAddress cannot be null");
+
+        final ObjectNode result = context.mapper().createObjectNode()
+                .put(NORTH, address.isNorth())
+                .put(LATITUDE_DEGREE, address.getLatitudeDegree())
+                .put(LATITUDE_MINUTE, address.getLatitudeMinute())
+                .put(LATITUDE_SECOND, address.getLatitudeSecond())
+                .put(EAST, address.isEast())
+                .put(LONGITUDE_DEGREE, address.getLongitudeDegree())
+                .put(LONGITUDE_MINUTE, address.getLongitudeMinute())
+                .put(LONGITUDE_SECOND, address.getLongitudeSecond())
+                .put(ALTITUDE, address.getAltitude());
+
+        if (address.getAddress() != null) {
+            final JsonCodec<MappingAddress> addressCodec =
+                    context.codec(MappingAddress.class);
+            ObjectNode addressNode = addressCodec.encode(address.getAddress(), context);
+            result.set(ADDRESS, addressNode);
+        }
+
+        return result;
+    }
+
+    @Override
+    public LispGcAddress decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        boolean north = nullIsIllegal(json.get(NORTH),
+                NORTH + MISSING_MEMBER_MESSAGE).asBoolean();
+        short latitudeDegree = (short) nullIsIllegal(json.get(LATITUDE_DEGREE),
+                LATITUDE_DEGREE + MISSING_MEMBER_MESSAGE).asInt();
+        byte latitudeMinute = (byte) nullIsIllegal(json.get(LATITUDE_MINUTE),
+                LATITUDE_MINUTE + MISSING_MEMBER_MESSAGE).asInt();
+        byte latitudeSecond = (byte) nullIsIllegal(json.get(LATITUDE_SECOND),
+                LATITUDE_SECOND + MISSING_MEMBER_MESSAGE).asInt();
+        boolean east = nullIsIllegal(json.get(EAST),
+                EAST + MISSING_MEMBER_MESSAGE).asBoolean();
+        short longitudeDegree = (short) nullIsIllegal(json.get(LONGITUDE_DEGREE),
+                LONGITUDE_DEGREE + MISSING_MEMBER_MESSAGE).asInt();
+        byte longitudeMinute = (byte) nullIsIllegal(json.get(LONGITUDE_MINUTE),
+                LONGITUDE_MINUTE + MISSING_MEMBER_MESSAGE).asInt();
+        byte longitudeSecond = (byte) nullIsIllegal(json.get(LONGITUDE_SECOND),
+                LONGITUDE_SECOND + MISSING_MEMBER_MESSAGE).asInt();
+        int altitude = nullIsIllegal(json.get(ALTITUDE),
+                ALTITUDE + MISSING_MEMBER_MESSAGE).asInt();
+
+        ObjectNode addressJson = get(json, ADDRESS);
+        MappingAddress mappingAddress = null;
+
+        if (addressJson != null) {
+            final JsonCodec<MappingAddress> addressCodec =
+                    context.codec(MappingAddress.class);
+            mappingAddress = addressCodec.decode(addressJson, context);
+        }
+
+        return new LispGcAddress.Builder()
+                            .withIsNorth(north)
+                            .withLatitudeDegree(latitudeDegree)
+                            .withLatitudeMinute(latitudeMinute)
+                            .withLatitudeSecond(latitudeSecond)
+                            .withIsEast(east)
+                            .withLongitudeDegree(longitudeDegree)
+                            .withLongitudeMinute(longitudeMinute)
+                            .withLongitudeSecond(longitudeSecond)
+                            .withAltitude(altitude)
+                            .withAddress(mappingAddress)
+                            .build();
+    }
+}
diff --git a/drivers/lisp/src/test/java/org/onosproject/drivers/lisp/extensions/codec/LispGcAddressCodecTest.java b/drivers/lisp/src/test/java/org/onosproject/drivers/lisp/extensions/codec/LispGcAddressCodecTest.java
new file mode 100644
index 0000000..356254b
--- /dev/null
+++ b/drivers/lisp/src/test/java/org/onosproject/drivers/lisp/extensions/codec/LispGcAddressCodecTest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2017-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.onosproject.drivers.lisp.extensions.codec;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.codec.impl.CodecManager;
+import org.onosproject.drivers.lisp.extensions.LispGcAddress;
+import org.onosproject.drivers.lisp.extensions.LispMappingExtensionCodecRegistrator;
+import org.onosproject.mapping.addresses.MappingAddresses;
+import org.onosproject.mapping.web.codec.MappingAddressJsonMatcher;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+/**
+ * Unit tests for LispGcAddressCodec.
+ */
+public class LispGcAddressCodecTest {
+
+    private static final boolean NORTH = true;
+    private static final short LATITUDE_DEGREE = (short) 1;
+    private static final byte LATITUDE_MINUTE = (byte) 1;
+    private static final byte LATITUDE_SECOND = (byte) 1;
+
+    private static final boolean EAST = false;
+    private static final short LONGITUDE_DEGREE = (short) 2;
+    private static final byte LONGITUDE_MINUTE = (byte) 2;
+    private static final byte LONGITUDE_SECOND = (byte) 2;
+
+    private static final int ALTITUDE = 3;
+    private static final IpPrefix IPV4_PREFIX = IpPrefix.valueOf("10.1.1.0/24");
+
+    private CodecContext context;
+    private JsonCodec<LispGcAddress> gcAddressCodec;
+    private LispMappingExtensionCodecRegistrator registrator;
+
+    /**
+     * Sets up for each test.
+     * Creates a context and fetches the LispGcAddress codec.
+     */
+    @Before
+    public void setUp() {
+        CodecManager manager = new CodecManager();
+        registrator = new LispMappingExtensionCodecRegistrator();
+        registrator.codecService = manager;
+        registrator.activate();
+
+        context = new LispMappingExtensionCodecContextAdapter(registrator.codecService);
+        gcAddressCodec = context.codec(LispGcAddress.class);
+        assertThat("Geo Coordinate address codec should not be null",
+                gcAddressCodec, notNullValue());
+    }
+
+    /**
+     * Deactivates the codec registrator.
+     */
+    @After
+    public void tearDown() {
+        registrator.deactivate();
+    }
+
+    /**
+     * Tests encoding of a LispGcAddress object.
+     */
+    @Test
+    public void testLispGcAddressEncode() {
+        LispGcAddress address = new LispGcAddress.Builder()
+                .withIsNorth(NORTH)
+                .withLatitudeDegree(LATITUDE_DEGREE)
+                .withLatitudeMinute(LATITUDE_MINUTE)
+                .withLatitudeSecond(LATITUDE_SECOND)
+                .withIsEast(EAST)
+                .withLongitudeDegree(LONGITUDE_DEGREE)
+                .withLongitudeMinute(LONGITUDE_MINUTE)
+                .withLongitudeSecond(LONGITUDE_SECOND)
+                .withAltitude(ALTITUDE)
+                .withAddress(MappingAddresses.ipv4MappingAddress(IPV4_PREFIX))
+                .build();
+        ObjectNode addressJson = gcAddressCodec.encode(address, context);
+        assertThat("errors in encoding Geo Coordinate address JSON",
+                addressJson, LispGcAddressJsonMatcher.matchesGcAddress(address));
+    }
+
+    /**
+     * Tests decoding of a LispGcAddress JSON object.
+     */
+    @Test
+    public void testLispGcAddressDecode() throws IOException {
+        LispGcAddress address = getLispGcAddress("LispGcAddress.json");
+
+        assertThat("incorrect north", address.isNorth(), is(NORTH));
+        assertThat("incorrect latitude degree",
+                address.getLatitudeDegree(), is(LATITUDE_DEGREE));
+        assertThat("incorrect latitude minute",
+                address.getLatitudeMinute(), is(LATITUDE_MINUTE));
+        assertThat("incorrect latitude second",
+                address.getLatitudeSecond(), is(LATITUDE_SECOND));
+        assertThat("incorrect east", address.isEast(), is(EAST));
+        assertThat("incorrect longitude degree",
+                address.getLongitudeDegree(), is(LONGITUDE_DEGREE));
+        assertThat("incorrect longitude minute",
+                address.getLongitudeMinute(), is(LONGITUDE_MINUTE));
+        assertThat("incorrect longitude second",
+                address.getLongitudeSecond(), is(LONGITUDE_SECOND));
+        assertThat("incorrect altitude", address.getAltitude(), is(ALTITUDE));
+        assertThat("incorrect mapping address", address.getAddress(),
+                is(MappingAddresses.ipv4MappingAddress(IPV4_PREFIX)));
+    }
+
+    /**
+     * Hamcrest matcher for LispGcAddress.
+     */
+    public static final class LispGcAddressJsonMatcher
+            extends TypeSafeDiagnosingMatcher<JsonNode> {
+
+        private final LispGcAddress address;
+
+        /**
+         * Default constructor.
+         *
+         * @param address LispGcAddress object
+         */
+        private LispGcAddressJsonMatcher(LispGcAddress address) {
+            this.address = address;
+        }
+
+        @Override
+        protected boolean matchesSafely(JsonNode jsonNode, Description description) {
+
+            // check north
+            boolean jsonNorth = jsonNode.get(LispGcAddressCodec.NORTH).asBoolean();
+            boolean north = address.isNorth();
+            if (jsonNorth != north) {
+                description.appendText("North was " + jsonNorth);
+                return false;
+            }
+
+            // check latitude degree
+            short jsonLatitudeDegree = (short) jsonNode.get(LispGcAddressCodec.LATITUDE_DEGREE).asInt();
+            short latitudeDegree = address.getLatitudeDegree();
+            if (jsonLatitudeDegree != latitudeDegree) {
+                description.appendText("Latitude degree was " + jsonLatitudeDegree);
+                return false;
+            }
+
+            // check latitude minute
+            byte jsonLatitudeMinute = (byte) jsonNode.get(LispGcAddressCodec.LATITUDE_MINUTE).asInt();
+            byte latitudeMinute = address.getLatitudeMinute();
+            if (jsonLatitudeMinute != latitudeMinute) {
+                description.appendText("Latitude minute was " + jsonLatitudeMinute);
+                return false;
+            }
+
+            // check latitude second
+            byte jsonLatitudeSecond = (byte) jsonNode.get(LispGcAddressCodec.LATITUDE_SECOND).asInt();
+            byte latitudeSecond = address.getLatitudeSecond();
+            if (jsonLatitudeSecond != latitudeSecond) {
+                description.appendText("Latitude second was " + jsonLatitudeSecond);
+                return false;
+            }
+
+            // check east
+            boolean jsonEast = jsonNode.get(LispGcAddressCodec.EAST).asBoolean();
+            boolean east = address.isEast();
+            if (jsonEast != east) {
+                description.appendText("East was " + jsonEast);
+                return false;
+            }
+
+            // check longitude degree
+            short jsonLongitudeDegree = (short) jsonNode.get(LispGcAddressCodec.LONGITUDE_DEGREE).asInt();
+            short longitudeDegree = address.getLongitudeDegree();
+            if (jsonLongitudeDegree != longitudeDegree) {
+                description.appendText("Longitude degree was " + jsonLongitudeDegree);
+                return false;
+            }
+
+            // check longitude minute
+            byte jsonLongitudeMinute = (byte) jsonNode.get(LispGcAddressCodec.LONGITUDE_MINUTE).asInt();
+            byte longitudeMinute = address.getLongitudeMinute();
+            if (jsonLongitudeMinute != longitudeMinute) {
+                description.appendText("Longitude minute was " + jsonLongitudeMinute);
+                return false;
+            }
+
+            // check longitude second
+            byte jsonLongitudeSecond = (byte) jsonNode.get(LispGcAddressCodec.LONGITUDE_SECOND).asInt();
+            byte longitudeSecond = address.getLongitudeSecond();
+            if (jsonLongitudeSecond != longitudeSecond) {
+                description.appendText("Longitude second was " + jsonLongitudeSecond);
+                return false;
+            }
+
+            // check altitude
+            int jsonAltitude = jsonNode.get(LispGcAddressCodec.ALTITUDE).asInt();
+            int altitude = address.getAltitude();
+            if (jsonAltitude != altitude) {
+                description.appendText("Altitude was " + jsonAltitude);
+                return false;
+            }
+
+            // check address
+            MappingAddressJsonMatcher addressMatcher =
+                    MappingAddressJsonMatcher.matchesMappingAddress(address.getAddress());
+
+            return addressMatcher.matches(jsonNode.get(LispGcAddressCodec.ADDRESS));
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText(address.toString());
+        }
+
+        /**
+         * Factory to allocate a LispGcAddress matcher.
+         *
+         * @param address LispGcAddress object we are looking for
+         * @return matcher
+         */
+        public static LispGcAddressJsonMatcher matchesGcAddress(LispGcAddress address) {
+            return new LispGcAddressJsonMatcher(address);
+        }
+    }
+
+    /**
+     * Reads in a LispGcAddress from the given resource and decodes it.
+     *
+     * @param resourceName resource to use to read the JSON for the rule
+     * @return decoded LispGcAddress
+     * @throws IOException if processing the resource fails
+     */
+    private LispGcAddress getLispGcAddress(String resourceName) throws IOException {
+        InputStream jsonStream = LispGcAddressCodecTest.class.getResourceAsStream(resourceName);
+        JsonNode json = context.mapper().readTree(jsonStream);
+        assertThat("JSON string should not be null", json, notNullValue());
+        LispGcAddress gcAddress = gcAddressCodec.decode((ObjectNode) json, context);
+        assertThat("Decoded address should not be null", gcAddress, notNullValue());
+        return gcAddress;
+    }
+}
diff --git a/drivers/lisp/src/test/resources/org/onosproject/drivers/lisp/extensions/codec/LispGcAddress.json b/drivers/lisp/src/test/resources/org/onosproject/drivers/lisp/extensions/codec/LispGcAddress.json
new file mode 100644
index 0000000..a4b9330
--- /dev/null
+++ b/drivers/lisp/src/test/resources/org/onosproject/drivers/lisp/extensions/codec/LispGcAddress.json
@@ -0,0 +1,15 @@
+{
+  "north": true,
+  "latitudeDegree": 1,
+  "latitudeMinute": 1,
+  "latitudeSecond": 1,
+  "east": false,
+  "longitudeDegree": 2,
+  "longitudeMinute": 2,
+  "longitudeSecond": 2,
+  "altitude": 3,
+  "address": {
+    "type": "IPV4",
+    "ipv4": "10.1.1.0/24"
+  }
+}
\ No newline at end of file