ONOS-3347 - HostMoved event now processed correctly.
- added new DefaultHashMap utility class
- updated TopoViewMsgHdlrBase for cleaner event translation.

Change-Id: I1c5e8c981e2d617366c25f497dc9336e09684a2e
diff --git a/utils/misc/src/main/java/org/onlab/util/DefaultHashMap.java b/utils/misc/src/main/java/org/onlab/util/DefaultHashMap.java
new file mode 100644
index 0000000..f9d878a
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/util/DefaultHashMap.java
@@ -0,0 +1,42 @@
+/*
+ *  Copyright 2015 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.onlab.util;
+
+import java.util.HashMap;
+
+/**
+ * HashMap that returns a default value for unmapped keys.
+ */
+public class DefaultHashMap<K, V> extends HashMap<K, V> {
+
+    /** Default value to return when no key binding exists. */
+    protected V defaultValue;
+
+    /**
+     * Constructs an empty map with the given default value.
+     *
+     * @param defaultValue the default value
+     */
+    public DefaultHashMap(V defaultValue) {
+        this.defaultValue = defaultValue;
+    }
+
+    @Override
+    public V get(Object k) {
+        return containsKey(k) ? super.get(k) : defaultValue;
+    }
+}
\ No newline at end of file
diff --git a/utils/misc/src/test/java/org/onlab/util/DefaultHashMapTest.java b/utils/misc/src/test/java/org/onlab/util/DefaultHashMapTest.java
new file mode 100644
index 0000000..db6b5fb
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/util/DefaultHashMapTest.java
@@ -0,0 +1,81 @@
+/*
+ *  Copyright 2015 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.onlab.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Unit tests for {@link DefaultHashMap}.
+ */
+public class DefaultHashMapTest {
+
+    private static final String ONE = "one";
+    private static final String TWO = "two";
+    private static final String THREE = "three";
+    private static final String FOUR = "four";
+
+    private static final String ALPHA = "Alpha";
+    private static final String BETA = "Beta";
+    private static final String OMEGA = "Omega";
+
+    private DefaultHashMap<String, Integer> map;
+    private DefaultHashMap<String, String> chartis;
+
+    private void loadMap() {
+        map.put(ONE, 1);
+        map.put(TWO, 2);
+    }
+
+    private void fortioCharti() {
+        chartis.put(ONE, ALPHA);
+        chartis.put(TWO, BETA);
+    }
+
+    @Test
+    public void nullDefaultIsAllowed() {
+        // but makes this class behave no different than HashMap
+        map = new DefaultHashMap<>(null);
+        loadMap();
+        assertEquals("missing 1", 1, (int) map.get(ONE));
+        assertEquals("missing 2", 2, (int) map.get(TWO));
+        assertEquals("three?", null, map.get(THREE));
+        assertEquals("four?", null, map.get(FOUR));
+    }
+
+    @Test
+    public void defaultToFive() {
+        map = new DefaultHashMap<>(5);
+        loadMap();
+        assertEquals("missing 1", 1, (int) map.get(ONE));
+        assertEquals("missing 2", 2, (int) map.get(TWO));
+        assertEquals("three?", 5, (int) map.get(THREE));
+        assertEquals("four?", 5, (int) map.get(FOUR));
+    }
+
+    @Test
+    public void defaultToOmega() {
+        chartis = new DefaultHashMap<>(OMEGA);
+        fortioCharti();
+        assertEquals("missing 1", ALPHA, chartis.get(ONE));
+        assertEquals("missing 2", BETA, chartis.get(TWO));
+        assertEquals("three?", OMEGA, chartis.get(THREE));
+        assertEquals("four?", OMEGA, chartis.get(FOUR));
+    }
+
+}
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 840e89f..8da8101 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
@@ -20,6 +20,7 @@
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.onlab.osgi.ServiceDirectory;
 import org.onlab.packet.IpAddress;
+import org.onlab.util.DefaultHashMap;
 import org.onosproject.cluster.ClusterEvent;
 import org.onosproject.cluster.ClusterService;
 import org.onosproject.cluster.ControllerNode;
@@ -80,17 +81,9 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Strings.isNullOrEmpty;
-import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
-import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_REMOVED;
 import static org.onosproject.cluster.ControllerNode.State.ACTIVE;
 import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
 import static org.onosproject.net.PortNumber.portNumber;
-import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
-import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_REMOVED;
-import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED;
-import static org.onosproject.net.host.HostEvent.Type.HOST_REMOVED;
-import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED;
-import static org.onosproject.net.link.LinkEvent.Type.LINK_REMOVED;
 import static org.onosproject.ui.topo.TopoConstants.CoreButtons;
 import static org.onosproject.ui.topo.TopoConstants.Properties;
 import static org.onosproject.ui.topo.TopoUtils.compactLinkString;
@@ -100,6 +93,33 @@
  */
 public abstract class TopologyViewMessageHandlerBase extends UiMessageHandler {
 
+    // default to an "add" event...
+    private static final DefaultHashMap<ClusterEvent.Type, String> CLUSTER_EVENT =
+            new DefaultHashMap<>("addInstance");
+
+    // default to an "update" event...
+    private static final DefaultHashMap<DeviceEvent.Type, String> DEVICE_EVENT =
+            new DefaultHashMap<>("updateDevice");
+    private static final DefaultHashMap<LinkEvent.Type, String> LINK_EVENT =
+            new DefaultHashMap<>("updateLink");
+    private static final DefaultHashMap<HostEvent.Type, String> HOST_EVENT =
+            new DefaultHashMap<>("updateHost");
+
+    // but call out specific events that we care to differentiate...
+    static {
+        CLUSTER_EVENT.put(ClusterEvent.Type.INSTANCE_REMOVED, "removeInstance");
+
+        DEVICE_EVENT.put(DeviceEvent.Type.DEVICE_ADDED, "addDevice");
+        DEVICE_EVENT.put(DeviceEvent.Type.DEVICE_REMOVED, "removeDevice");
+
+        LINK_EVENT.put(LinkEvent.Type.LINK_ADDED, "addLink");
+        LINK_EVENT.put(LinkEvent.Type.LINK_REMOVED, "removeLink");
+
+        HOST_EVENT.put(HostEvent.Type.HOST_ADDED, "addHost");
+        HOST_EVENT.put(HostEvent.Type.HOST_REMOVED, "removeHost");
+        HOST_EVENT.put(HostEvent.Type.HOST_MOVED, "moveHost");
+    }
+
     protected static final Logger log =
             LoggerFactory.getLogger(TopologyViewMessageHandlerBase.class);
 
@@ -204,7 +224,7 @@
     }
 
     // Produces a cluster instance message to the client.
-    protected ObjectNode instanceMessage(ClusterEvent event, String messageType) {
+    protected ObjectNode instanceMessage(ClusterEvent event, String msgType) {
         ControllerNode node = event.subject();
         int switchCount = mastershipService.getDevicesOf(node.id()).size();
         ObjectNode payload = objectNode()
@@ -222,10 +242,7 @@
         payload.set("labels", labels);
         addMetaUi(node.id().toString(), payload);
 
-        String type = messageType != null ? messageType :
-                ((event.type() == INSTANCE_ADDED) ? "addInstance" :
-                        ((event.type() == INSTANCE_REMOVED ? "removeInstance" :
-                                "addInstance")));
+        String type = msgType != null ? msgType : CLUSTER_EVENT.get(event.type());
         return JsonUtils.envelope(type, 0, payload);
     }
 
@@ -251,8 +268,7 @@
         addGeoLocation(device, payload);
         addMetaUi(device.id().toString(), payload);
 
-        String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
-                ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
+        String type = DEVICE_EVENT.get(event.type());
         return JsonUtils.envelope(type, 0, payload);
     }
 
@@ -268,8 +284,7 @@
                 .put("srcPort", link.src().port().toString())
                 .put("dst", link.dst().deviceId().toString())
                 .put("dstPort", link.dst().port().toString());
-        String type = (event.type() == LINK_ADDED) ? "addLink" :
-                ((event.type() == LINK_REMOVED) ? "removeLink" : "updateLink");
+        String type = LINK_EVENT.get(event.type());
         return JsonUtils.envelope(type, 0, payload);
     }
 
@@ -277,20 +292,24 @@
     protected ObjectNode hostMessage(HostEvent event) {
         Host host = event.subject();
         String hostType = host.annotations().value(AnnotationKeys.TYPE);
+        HostLocation prevLoc = event.prevLocation();
+
         ObjectNode payload = objectNode()
                 .put("id", host.id().toString())
                 .put("type", isNullOrEmpty(hostType) ? "endstation" : hostType)
                 .put("ingress", compactLinkString(edgeLink(host, true)))
                 .put("egress", compactLinkString(edgeLink(host, false)));
         payload.set("cp", hostConnect(host.location()));
+        if (prevLoc != null) {
+            payload.set("prevCp", hostConnect(event.prevLocation()));
+        }
         payload.set("labels", labels(ip(host.ipAddresses()),
                                      host.mac().toString()));
         payload.set("props", props(host.annotations()));
         addGeoLocation(host, payload);
         addMetaUi(host.id().toString(), payload);
 
-        String type = (event.type() == HOST_ADDED) ? "addHost" :
-                ((event.type() == HOST_REMOVED) ? "removeHost" : "updateHost");
+        String type = HOST_EVENT.get(event.type());
         return JsonUtils.envelope(type, 0, payload);
     }
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
index 2957629..9b07f87 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoEvent.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -55,6 +55,7 @@
             removeDevice: tfs,
             addHost: tfs,
             updateHost: tfs,
+            moveHost: tfs,
             removeHost: tfs,
             addLink: tfs,
             updateLink: tfs,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index 844d7dc..175ee79 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -187,6 +187,35 @@
         }
     }
 
+    function moveHost(data) {
+        var id = data.id,
+            d = lu[id],
+            lnk;
+        if (d) {
+            // first remove the old host link
+            removeLinkElement(d.linkData);
+
+            // merge new data
+            angular.extend(d, data);
+            if (tms.positionNode(d, true)) {
+                sendUpdateMeta(d);
+            }
+
+            // now create a new host link
+            lnk = tms.createHostLink(data);
+            if (lnk) {
+                d.linkData = lnk;
+                network.links.push(lnk);
+                lu[d.ingress] = lnk;
+                lu[d.egress] = lnk;
+            }
+
+            updateNodes();
+            updateLinks();
+            fResume();
+        }
+    }
+
     function removeHost(data) {
         var id = data.id,
             d = lu[id];
@@ -1142,6 +1171,7 @@
                 removeDevice: removeDevice,
                 addHost: addHost,
                 updateHost: updateHost,
+                moveHost: moveHost,
                 removeHost: removeHost,
                 addLink: addLink,
                 updateLink: updateLink,