ONOS-6730: Topo View i18n:
- Deprecate non-localized PropertyPanel.addProp() methods.
- Add modify*LinkDetails() methods to UiTopoOverlay class.
- Augment TVMH.RequestDetails to handle link details requests.
- Refactor deviceDetails() to allow piecemeal construction of the Properties Panel.
    This allows us to include (or not) the location properties (geo/grid).
- Refactor hostDetails() for piecemeal construction of Properties Panel.
- Add edgeLinkDetails() and infraLinkDetails() methods.
- No lat/long suppression now done server-side. Check for trailing separator.
- Augment requestDetails() to format link details requests.
- Added lion.getSafe(Enum<?>) method.
- Added DeviceEnums and LinkEnums resource bundles.

Change-Id: Ibbd113a7d5ef73765cd10aed0fb7ea8efbaa16c5
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
index 3cbce06..a359964 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
@@ -80,6 +80,7 @@
 import static java.util.concurrent.Executors.newSingleThreadExecutor;
 import static org.onlab.util.Tools.groupedThreads;
 import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
+import static org.onosproject.net.ConnectPoint.deviceConnectPoint;
 import static org.onosproject.net.DeviceId.deviceId;
 import static org.onosproject.net.HostId.hostId;
 import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
@@ -137,10 +138,16 @@
     private static final String EXTRA = "extra";
     private static final String ID = "id";
     private static final String KEY = "key";
+    private static final String IS_EDGE_LINK = "isEdgeLink";
+    private static final String SOURCE_ID = "sourceId";
+    private static final String SOURCE_PORT = "sourcePort";
+    private static final String TARGET_ID = "targetId";
+    private static final String TARGET_PORT = "targetPort";
     private static final String APP_ID = "appId";
     private static final String APP_NAME = "appName";
     private static final String DEVICE = "device";
     private static final String HOST = "host";
+    private static final String LINK = "link";
     private static final String CLASS = "class";
     private static final String UNKNOWN = "unknown";
     private static final String ONE = "one";
@@ -162,6 +169,8 @@
 
     private static final String MY_APP_ID = "org.onosproject.gui";
 
+    private static final String SLASH = "/";
+
     private static final long TRAFFIC_PERIOD = 5000;
     private static final long SUMMARY_PERIOD = 30000;
 
@@ -362,20 +371,48 @@
         @Override
         public void process(ObjectNode payload) {
             String type = string(payload, CLASS, UNKNOWN);
-            String id = string(payload, ID);
+            String id = string(payload, ID, "");
             PropertyPanel pp = null;
 
             if (type.equals(DEVICE)) {
                 DeviceId did = deviceId(id);
                 pp = deviceDetails(did);
                 overlayCache.currentOverlay().modifyDeviceDetails(pp, did);
+
             } else if (type.equals(HOST)) {
                 HostId hid = hostId(id);
                 pp = hostDetails(hid);
                 overlayCache.currentOverlay().modifyHostDetails(pp, hid);
+
+            } else if (type.equals(LINK)) {
+                String srcId = string(payload, SOURCE_ID);
+                String tgtId = string(payload, TARGET_ID);
+                boolean isEdgeLink = bool(payload, IS_EDGE_LINK);
+
+                if (isEdgeLink) {
+                    HostId hid = hostId(srcId);
+                    String cpstr = tgtId + SLASH + string(payload, TARGET_PORT);
+                    ConnectPoint cp = deviceConnectPoint(cpstr);
+
+                    pp = edgeLinkDetails(hid, cp);
+                    overlayCache.currentOverlay().modifyEdgeLinkDetails(pp, hid, cp);
+
+                } else {
+                    String cpAstr = srcId + SLASH + string(payload, SOURCE_PORT);
+                    String cpBstr = tgtId + SLASH + string(payload, TARGET_PORT);
+                    ConnectPoint cpA = deviceConnectPoint(cpAstr);
+                    ConnectPoint cpB = deviceConnectPoint(cpBstr);
+
+                    pp = infraLinkDetails(cpA, cpB);
+                    overlayCache.currentOverlay().modifyInfraLinkDetails(pp, cpA, cpB);
+                }
             }
 
-            sendMessage(envelope(SHOW_DETAILS, json(pp)));
+            if (pp != null) {
+                sendMessage(envelope(SHOW_DETAILS, json(pp)));
+            } else {
+                log.warn("Unable to process details request: {}", payload);
+            }
         }
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
index c2e290d..c3eae02 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableSet;
 import org.onlab.osgi.ServiceDirectory;
 import org.onlab.packet.IpAddress;
+import org.onlab.packet.VlanId;
 import org.onlab.util.DefaultHashMap;
 import org.onosproject.cluster.ClusterEvent;
 import org.onosproject.cluster.ControllerNode;
@@ -67,15 +68,29 @@
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static org.onosproject.net.PortNumber.portNumber;
 import static org.onosproject.ui.topo.TopoConstants.CoreButtons;
-import static org.onosproject.ui.topo.TopoConstants.Properties;
 import static org.onosproject.ui.topo.TopoConstants.Properties.DEVICES;
 import static org.onosproject.ui.topo.TopoConstants.Properties.FLOWS;
+import static org.onosproject.ui.topo.TopoConstants.Properties.GRID_X;
+import static org.onosproject.ui.topo.TopoConstants.Properties.GRID_Y;
 import static org.onosproject.ui.topo.TopoConstants.Properties.HOSTS;
+import static org.onosproject.ui.topo.TopoConstants.Properties.HW_VERSION;
 import static org.onosproject.ui.topo.TopoConstants.Properties.INTENTS;
+import static org.onosproject.ui.topo.TopoConstants.Properties.IP;
+import static org.onosproject.ui.topo.TopoConstants.Properties.LATITUDE;
 import static org.onosproject.ui.topo.TopoConstants.Properties.LINKS;
+import static org.onosproject.ui.topo.TopoConstants.Properties.LONGITUDE;
+import static org.onosproject.ui.topo.TopoConstants.Properties.MAC;
+import static org.onosproject.ui.topo.TopoConstants.Properties.PORTS;
+import static org.onosproject.ui.topo.TopoConstants.Properties.PROTOCOL;
+import static org.onosproject.ui.topo.TopoConstants.Properties.SERIAL_NUMBER;
+import static org.onosproject.ui.topo.TopoConstants.Properties.SW_VERSION;
 import static org.onosproject.ui.topo.TopoConstants.Properties.TOPOLOGY_SSCS;
 import static org.onosproject.ui.topo.TopoConstants.Properties.TUNNELS;
+import static org.onosproject.ui.topo.TopoConstants.Properties.URI;
+import static org.onosproject.ui.topo.TopoConstants.Properties.VENDOR;
 import static org.onosproject.ui.topo.TopoConstants.Properties.VERSION;
+import static org.onosproject.ui.topo.TopoConstants.Properties.VLAN;
+import static org.onosproject.ui.topo.TopoConstants.Properties.VLAN_NONE;
 import static org.onosproject.ui.topo.TopoUtils.compactLinkString;
 
 /**
@@ -85,11 +100,32 @@
 
     private static final String NO_GEO_VALUE = "0.0";
     private static final String DASH = "-";
+    private static final String SLASH = " / ";
 
     // nav paths are the view names for hot-link navigation from topo view...
     private static final String DEVICE_NAV_PATH = "device";
     private static final String HOST_NAV_PATH = "host";
 
+    // link panel label keys
+    private static final String LPL_FRIENDLY = "lp_label_friendly";
+    private static final String LPL_A_TYPE = "lp_label_a_type";
+    private static final String LPL_A_ID = "lp_label_a_id";
+    private static final String LPL_A_FRIENDLY = "lp_label_a_friendly";
+    private static final String LPL_A_PORT = "lp_label_a_port";
+    private static final String LPL_B_TYPE = "lp_label_b_type";
+    private static final String LPL_B_ID = "lp_label_b_id";
+    private static final String LPL_B_FRIENDLY = "lp_label_b_friendly";
+    private static final String LPL_B_PORT = "lp_label_b_port";
+    private static final String LPL_A2B = "lp_label_a2b";
+    private static final String LPL_B2A = "lp_label_b2a";
+    private static final String LPV_NO_LINK = "lp_value_no_link";
+
+    // other Lion keys
+    private static final String HOST = "host";
+    private static final String DEVICE = "device";
+    private static final String EXPECTED = "expected";
+    private static final String NOT_EXPECTED = "not_expected";
+
     // default to an "add" event...
     private static final DefaultHashMap<ClusterEvent.Type, String> CLUSTER_EVENT =
             new DefaultHashMap<>("addInstance");
@@ -117,6 +153,32 @@
         HOST_EVENT.put(HostEvent.Type.HOST_MOVED, "moveHost");
     }
 
+    private static final DefaultHashMap<Device.Type, String> DEVICE_GLYPHS =
+            new DefaultHashMap<>("m_unknown");
+
+    static {
+        DEVICE_GLYPHS.put(Device.Type.SWITCH, "m_switch");
+        DEVICE_GLYPHS.put(Device.Type.ROUTER, "m_router");
+        DEVICE_GLYPHS.put(Device.Type.ROADM, "m_roadm");
+        DEVICE_GLYPHS.put(Device.Type.OTN, "m_otn");
+        DEVICE_GLYPHS.put(Device.Type.ROADM_OTN, "m_roadm_otn");
+        DEVICE_GLYPHS.put(Device.Type.BALANCER, "m_balancer");
+        DEVICE_GLYPHS.put(Device.Type.IPS, "m_ips");
+        DEVICE_GLYPHS.put(Device.Type.IDS, "m_ids");
+        DEVICE_GLYPHS.put(Device.Type.CONTROLLER, "m_controller");
+        DEVICE_GLYPHS.put(Device.Type.VIRTUAL, "m_virtual");
+        DEVICE_GLYPHS.put(Device.Type.FIBER_SWITCH, "m_fiberSwitch");
+        DEVICE_GLYPHS.put(Device.Type.MICROWAVE, "m_microwave");
+        DEVICE_GLYPHS.put(Device.Type.OLT, "m_olt");
+        DEVICE_GLYPHS.put(Device.Type.ONU, "m_onu");
+        DEVICE_GLYPHS.put(Device.Type.OPTICAL_AMPLIFIER, "unknown"); // TODO glyph needed
+        DEVICE_GLYPHS.put(Device.Type.OTHER, "m_other");
+    }
+
+    private static final String DEFAULT_HOST_GLYPH = "m_endstation";
+    private static final String LINK_GLYPH = "m_ports";
+
+
     protected static final Logger log =
             LoggerFactory.getLogger(TopologyViewMessageHandlerBase.class);
 
@@ -368,13 +430,19 @@
     // -----------------------------------------------------------------------
     // Create models of the data to return, that overlays can adjust / augment
 
+    private String lookupGlyph(Device device) {
+        return DEVICE_GLYPHS.get(device.type());
+    }
+
+
     // Returns property panel model for summary response.
     protected PropertyPanel summmaryMessage() {
+        // chose NOT to add debug messages, since this is called every few seconds
         Topology topology = services.topology().currentTopology();
         LionBundle lion = getLionBundle(LION_TOPO);
         String panelTitle = lion.getSafe("title_panel_summary");
 
-        return new PropertyPanel(panelTitle, "node")
+        return new PropertyPanel(panelTitle, "bird")
                 .addProp(VERSION, lion.getSafe(VERSION), version)
                 .addSeparator()
                 .addProp(DEVICES, lion.getSafe(DEVICES), services.device().getDeviceCount())
@@ -387,39 +455,79 @@
                 .addProp(FLOWS, lion.getSafe(FLOWS), services.flow().getFlowRuleCount());
     }
 
-    // Returns property panel model for device details response.
-    protected PropertyPanel deviceDetails(DeviceId deviceId) {
+
+    private String friendlyDevice(DeviceId deviceId) {
         Device device = services.device().getDevice(deviceId);
         Annotations annot = device.annotations();
         String name = annot.value(AnnotationKeys.NAME);
+        return isNullOrEmpty(name) ? deviceId.toString() : name;
+    }
+
+    // Generates a property panel model for device details response
+    protected PropertyPanel deviceDetails(DeviceId deviceId) {
+        log.debug("generate prop panel data for device {}", deviceId);
+        Device device = services.device().getDevice(deviceId);
+        Annotations annot = device.annotations();
+        String proto = annot.value(AnnotationKeys.PROTOCOL);
+        String title = friendlyDevice(deviceId);
+        LionBundle lion = getLionBundle(LION_TOPO);
+
+        PropertyPanel pp = new PropertyPanel(title, lookupGlyph(device))
+                .navPath(DEVICE_NAV_PATH)
+                .id(deviceId.toString());
+        addDeviceBasicProps(pp, deviceId, device, proto, lion);
+        addLocationProps(pp, annot, lion);
+        addDeviceCountStats(pp, deviceId, lion);
+        addDeviceCoreButtons(pp);
+        return pp;
+    }
+
+    private void addDeviceBasicProps(PropertyPanel pp, DeviceId deviceId,
+                                     Device device, String proto, LionBundle lion) {
+        pp.addProp(URI, lion.getSafe(URI), deviceId.toString())
+                .addProp(VENDOR, lion.getSafe(VENDOR), device.manufacturer())
+                .addProp(HW_VERSION, lion.getSafe(HW_VERSION), device.hwVersion())
+                .addProp(SW_VERSION, lion.getSafe(SW_VERSION), device.swVersion())
+                .addProp(SERIAL_NUMBER, lion.getSafe(SERIAL_NUMBER), device.serialNumber())
+                .addProp(PROTOCOL, lion.getSafe(PROTOCOL), proto)
+                .addSeparator();
+    }
+
+    // only add location properties if we have them
+    private void addLocationProps(PropertyPanel pp, Annotations annot,
+                                  LionBundle lion) {
+        String slat = annot.value(AnnotationKeys.LATITUDE);
+        String slng = annot.value(AnnotationKeys.LONGITUDE);
+        String sgrY = annot.value(AnnotationKeys.GRID_Y);
+        String sgrX = annot.value(AnnotationKeys.GRID_X);
+
+        boolean validLat = slat != null && !slat.equals(NO_GEO_VALUE);
+        boolean validLng = slng != null && !slng.equals(NO_GEO_VALUE);
+        if (validLat && validLng) {
+            pp.addProp(LATITUDE, lion.getSafe(LATITUDE), slat)
+                    .addProp(LONGITUDE, lion.getSafe(LONGITUDE), slng)
+                    .addSeparator();
+
+        } else if (sgrY != null && sgrX != null) {
+            pp.addProp(GRID_Y, lion.getSafe(GRID_Y), sgrY)
+                    .addProp(GRID_X, lion.getSafe(GRID_X), sgrX)
+                    .addSeparator();
+        }
+        // else, no location
+    }
+
+    private void addDeviceCountStats(PropertyPanel pp, DeviceId deviceId, LionBundle lion) {
         int portCount = services.device().getPorts(deviceId).size();
         int flowCount = getFlowCount(deviceId);
         int tunnelCount = getTunnelCount(deviceId);
 
-        String title = isNullOrEmpty(name) ? deviceId.toString() : name;
-        String typeId = device.type().toString().toLowerCase();
+        pp.addProp(PORTS, lion.getSafe(PORTS), portCount)
+                .addProp(FLOWS, lion.getSafe(FLOWS), flowCount)
+                .addProp(TUNNELS, lion.getSafe(TUNNELS), tunnelCount);
+    }
 
-        return new PropertyPanel(title, typeId)
-                .navPath(DEVICE_NAV_PATH)
-                .id(deviceId.toString())
-
-                .addProp(Properties.URI, deviceId.toString())
-                .addProp(Properties.VENDOR, device.manufacturer())
-                .addProp(Properties.HW_VERSION, device.hwVersion())
-                .addProp(Properties.SW_VERSION, device.swVersion())
-                .addProp(Properties.SERIAL_NUMBER, device.serialNumber())
-                .addProp(Properties.PROTOCOL, annot.value(AnnotationKeys.PROTOCOL))
-                .addSeparator()
-
-                .addProp(Properties.LATITUDE, annot.value(AnnotationKeys.LATITUDE))
-                .addProp(Properties.LONGITUDE, annot.value(AnnotationKeys.LONGITUDE))
-                .addSeparator()
-
-                .addProp(Properties.PORTS, portCount)
-                .addProp(FLOWS, flowCount)
-                .addProp(TUNNELS, tunnelCount)
-
-                .addButton(CoreButtons.SHOW_DEVICE_VIEW)
+    private void addDeviceCoreButtons(PropertyPanel pp) {
+        pp.addButton(CoreButtons.SHOW_DEVICE_VIEW)
                 .addButton(CoreButtons.SHOW_FLOW_VIEW)
                 .addButton(CoreButtons.SHOW_PORT_VIEW)
                 .addButton(CoreButtons.SHOW_GROUP_VIEW)
@@ -467,23 +575,108 @@
         return useDefaultName(name) ? ip(host.ipAddresses()) : name;
     }
 
-    // Returns host details response.
-    protected PropertyPanel hostDetails(HostId hostId) {
-        Host host = services.host().getHost(hostId);
-        Annotations annot = host.annotations();
-        String type = annot.value(AnnotationKeys.TYPE);
-        String vlan = host.vlan().toString();
-        String typeId = isNullOrEmpty(type) ? "endstation" : type;
-
-        return new PropertyPanel(nameForHost(host), typeId)
-                .navPath(HOST_NAV_PATH)
-                .id(hostId.toString())
-                .addProp(Properties.MAC, host.mac())
-                .addProp(Properties.IP, host.ipAddresses(), "[\\[\\]]")
-                .addProp(Properties.VLAN, "-1".equals(vlan) ? "none" : vlan)
-                .addSeparator()
-                .addProp(Properties.LATITUDE, annot.value(AnnotationKeys.LATITUDE))
-                .addProp(Properties.LONGITUDE, annot.value(AnnotationKeys.LONGITUDE));
+    private String glyphForHost(Annotations annot) {
+        String uiType = annot.value(AnnotationKeys.UI_TYPE);
+        return isNullOrEmpty(uiType) ? DEFAULT_HOST_GLYPH : uiType;
     }
 
+    // Generates a property panel model for a host details response
+    protected PropertyPanel hostDetails(HostId hostId) {
+        log.debug("generate prop panel data for host {}", hostId);
+        Host host = services.host().getHost(hostId);
+        Annotations annot = host.annotations();
+        String glyphId = glyphForHost(annot);
+        LionBundle lion = getLionBundle(LION_TOPO);
+
+        PropertyPanel pp = new PropertyPanel(nameForHost(host), glyphId)
+                .navPath(HOST_NAV_PATH)
+                .id(hostId.toString());
+        addHostBasicProps(pp, host, lion);
+        addLocationProps(pp, annot, lion);
+        return pp;
+    }
+
+    private void addHostBasicProps(PropertyPanel pp, Host host, LionBundle lion) {
+        pp.addProp(LPL_FRIENDLY, lion.getSafe(LPL_FRIENDLY), nameForHost(host))
+                .addProp(MAC, lion.getSafe(MAC), host.mac())
+                .addProp(IP, lion.getSafe(IP), host.ipAddresses(), "[\\[\\]]")
+                .addProp(VLAN, lion.getSafe(VLAN), displayVlan(host.vlan(), lion))
+                .addSeparator();
+    }
+
+    private String displayVlan(VlanId vlan, LionBundle lion) {
+        return VlanId.NONE.equals(vlan) ? lion.getSafe(VLAN_NONE) : vlan.toString();
+    }
+
+    // Generates a property panel model for a link details response (edge-link)
+    protected PropertyPanel edgeLinkDetails(HostId hid, ConnectPoint cp) {
+        log.debug("generate prop panel data for edgelink {} {}", hid, cp);
+        LionBundle lion = getLionBundle(LION_TOPO);
+        String title = lion.getSafe("title_edge_link");
+
+        PropertyPanel pp = new PropertyPanel(title, LINK_GLYPH);
+        addLinkHostProps(pp, hid, lion);
+        addLinkCpBProps(pp, cp, lion);
+        return pp;
+    }
+
+    // Generates a property panel model for a link details response (infra-link)
+    protected PropertyPanel infraLinkDetails(ConnectPoint cpA, ConnectPoint cpB) {
+        log.debug("generate prop panel data for infralink {} {}", cpA, cpB);
+        LionBundle lion = getLionBundle(LION_TOPO);
+        String title = lion.getSafe("title_infra_link");
+
+        PropertyPanel pp = new PropertyPanel(title, LINK_GLYPH);
+        addLinkCpAProps(pp, cpA, lion);
+        addLinkCpBProps(pp, cpB, lion);
+        addLinkBackingProps(pp, cpA, cpB, lion);
+        return pp;
+    }
+
+    private void addLinkHostProps(PropertyPanel pp, HostId hostId, LionBundle lion) {
+        Host host = services.host().getHost(hostId);
+
+        pp.addProp(LPL_A_TYPE, lion.getSafe(LPL_A_TYPE), lion.getSafe(HOST))
+                .addProp(LPL_A_ID, lion.getSafe(LPL_A_ID), hostId.toString())
+                .addProp(LPL_A_FRIENDLY, lion.getSafe(LPL_A_FRIENDLY), nameForHost(host))
+                .addSeparator();
+    }
+
+    private void addLinkCpAProps(PropertyPanel pp, ConnectPoint cp, LionBundle lion) {
+        DeviceId did = cp.deviceId();
+
+        pp.addProp(LPL_A_TYPE, lion.getSafe(LPL_A_TYPE), lion.getSafe(DEVICE))
+                .addProp(LPL_A_ID, lion.getSafe(LPL_A_ID), did.toString())
+                .addProp(LPL_A_FRIENDLY, lion.getSafe(LPL_A_FRIENDLY), friendlyDevice(did))
+                .addProp(LPL_A_PORT, lion.getSafe(LPL_A_PORT), cp.port().toLong())
+                .addSeparator();
+    }
+
+    private void addLinkCpBProps(PropertyPanel pp, ConnectPoint cp, LionBundle lion) {
+        DeviceId did = cp.deviceId();
+
+        pp.addProp(LPL_B_TYPE, lion.getSafe(LPL_B_TYPE), lion.getSafe(DEVICE))
+                .addProp(LPL_B_ID, lion.getSafe(LPL_B_ID), did.toString())
+                .addProp(LPL_B_FRIENDLY, lion.getSafe(LPL_B_FRIENDLY), friendlyDevice(did))
+                .addProp(LPL_B_PORT, lion.getSafe(LPL_B_PORT), cp.port().toLong())
+                .addSeparator();
+    }
+
+    private void addLinkBackingProps(PropertyPanel pp, ConnectPoint cpA,
+                                     ConnectPoint cpB, LionBundle lion) {
+        Link a2b = services.link().getLink(cpA, cpB);
+        Link b2a = services.link().getLink(cpB, cpA);
+
+        pp.addProp(LPL_A2B, lion.getSafe(LPL_A2B), linkPropString(a2b, lion))
+                .addProp(LPL_B2A, lion.getSafe(LPL_B2A), linkPropString(b2a, lion));
+    }
+
+    private String linkPropString(Link link, LionBundle lion) {
+        if (link == null) {
+            return lion.getSafe(LPV_NO_LINK);
+        }
+        return lion.getSafe(link.type()) + SLASH +
+                lion.getSafe(link.state()) + SLASH +
+                lion.getSafe(link.isExpected() ? EXPECTED : NOT_EXPECTED);
+    }
 }