ONOS-4970: Device data for topology view -- WIP.

Change-Id: Ie5a0c65f38b32672570919c50c1f53b14d293d3f
diff --git a/core/api/src/main/java/org/onosproject/net/AnnotationKeys.java b/core/api/src/main/java/org/onosproject/net/AnnotationKeys.java
index 4235ed1..4b82bf5 100644
--- a/core/api/src/main/java/org/onosproject/net/AnnotationKeys.java
+++ b/core/api/src/main/java/org/onosproject/net/AnnotationKeys.java
@@ -50,7 +50,7 @@
     public static final String LATITUDE = "latitude";
 
     /**
-     * Annotation key for longitute (e.g. longitude of device).
+     * Annotation key for longitude (e.g. longitude of device).
      */
     public static final String LONGITUDE = "longitude";
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
index a5a344a..6d8ae53 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
@@ -26,6 +26,9 @@
 import org.onosproject.incubator.net.PortStatisticsService;
 import org.onosproject.incubator.net.tunnel.TunnelService;
 import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.Annotated;
+import org.onosproject.net.Annotations;
+import org.onosproject.net.Device;
 import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.flow.FlowRuleService;
 import org.onosproject.net.host.HostService;
@@ -51,8 +54,11 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.AnnotationKeys.LATITUDE;
+import static org.onosproject.net.AnnotationKeys.LONGITUDE;
 import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
 
 /**
@@ -88,6 +94,11 @@
     private TunnelService tunnelService;
 
 
+    // NOTE: we'll stick this here for now, but maybe there is a better home?
+    //       (this is not distributed across the cluster)
+    private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
+
+
     /**
      * Creates an instance with a reference to the services directory, so that
      * additional information about network elements may be looked up on
@@ -263,17 +274,73 @@
                 .put("master", nullIsEmpty(device.master()))
                 .put("layer", device.layer());
 
-        // TODO: complete device details
-//        addLabels(node, device);
-//        addProps(node, device);
-//        addGeoLocation(node, device);
-//        addMetaUi(node, device);
+        Device d = device.backingDevice();
+
+        addProps(node, d);
+        addGeoLocation(node, d);
+        addMetaUi(node, device.idAsString());
 
         return node;
     }
 
-    private void addLabels(ObjectNode node, UiDevice device) {
+    private void addProps(ObjectNode node, Device dev) {
+        Annotations annot = dev.annotations();
+        ObjectNode props = objectNode();
+        if (annot != null) {
+            annot.keys().forEach(k -> props.put(k, annot.value(k)));
+        }
+        node.set("props", props);
+    }
 
+    private void addMetaUi(ObjectNode node, String metaInstanceId) {
+        ObjectNode meta = metaUi.get(metaInstanceId);
+        if (meta != null) {
+            node.set("metaUi", meta);
+        }
+    }
+
+    private void addGeoLocation(ObjectNode node, Annotated a) {
+        List<String> lngLat = getAnnotValues(a, LONGITUDE, LATITUDE);
+        if (lngLat != null) {
+            try {
+                double lng = Double.parseDouble(lngLat.get(0));
+                double lat = Double.parseDouble(lngLat.get(1));
+                ObjectNode loc = objectNode()
+                        .put("type", "lnglat")
+                        .put("lng", lng)
+                        .put("lat", lat);
+                node.set("location", loc);
+
+            } catch (NumberFormatException e) {
+                log.warn("Invalid geo data: longitude={}, latitude={}",
+                        lngLat.get(0), lngLat.get(1));
+            }
+        } else {
+            log.debug("No geo lng/lat for {}", a);
+        }
+    }
+
+    // return list of string values from annotated instance, for given keys
+    // return null if any keys are not present
+    List<String> getAnnotValues(Annotated a, String... annotKeys) {
+        List<String> result = new ArrayList<>(annotKeys.length);
+        for (String k : annotKeys) {
+            String v = a.annotations().value(k);
+            if (v == null) {
+                return null;
+            }
+            result.add(v);
+        }
+        return result;
+    }
+
+    // derive JSON object from annotations
+    private ObjectNode props(Annotations annotations) {
+        ObjectNode p = objectNode();
+        if (annotations != null) {
+            annotations.keys().forEach(k -> p.put(k, annotations.value(k)));
+        }
+        return p;
     }
 
     private ObjectNode json(UiHost host) {
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java
index 1a06c62..ab0cd2d 100644
--- a/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java
@@ -19,6 +19,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import org.junit.Test;
+import org.onosproject.net.Annotated;
+import org.onosproject.net.Annotations;
 import org.onosproject.ui.impl.AbstractUiImplTest;
 import org.onosproject.ui.model.topo.UiNode;
 
@@ -26,6 +28,7 @@
 import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
 import static org.onosproject.ui.model.topo.UiNode.LAYER_OPTICAL;
 import static org.onosproject.ui.model.topo.UiNode.LAYER_PACKET;
@@ -145,4 +148,59 @@
         assertEquals("missing node B", true, def.contains(NODE_B));
         assertEquals("missing node E", true, def.contains(NODE_E));
     }
+
+    private static final String K1 = "K1";
+    private static final String K2 = "K2";
+    private static final String K3 = "K3";
+    private static final String K4 = "K4";
+
+    private static final String V1 = "V1";
+    private static final String V2 = "V2";
+    private static final String V3 = "V3";
+
+    private static final Annotations ANNOTS = new Annotations() {
+        @Override
+        public Set<String> keys() {
+            return ImmutableSet.of(K1, K2, K3);
+        }
+
+        @Override
+        public String value(String key) {
+            switch (key) {
+                case K1:
+                    return V1;
+                case K2:
+                    return V2;
+                case K3:
+                    return V3;
+                default:
+                    return null;
+            }
+        }
+    };
+
+    private static final Annotated THING = () -> ANNOTS;
+
+    private void verifyValues(List<String> vals, String... exp) {
+        print(vals);
+        if (exp.length == 0) {
+            // don't expect any results
+            assertNull("huh?", vals);
+        } else {
+            assertEquals("wrong list len", exp.length, vals.size());
+
+            for (int i = 0; i < exp.length; i++) {
+                assertEquals("wrong value " + i, exp[i], vals.get(i));
+            }
+        }
+    }
+
+    @Test
+    public void annotValues() {
+        print("annotValues()");
+        verifyValues(t2.getAnnotValues(THING, K1), V1);
+        verifyValues(t2.getAnnotValues(THING, K3, K1), V3, V1);
+        verifyValues(t2.getAnnotValues(THING, K1, K2, K3), V1, V2, V3);
+        verifyValues(t2.getAnnotValues(THING, K1, K4));
+    }
 }