Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
index 4abefa7..c54e3f4 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
@@ -30,6 +30,7 @@
 import org.onlab.onos.net.host.HostService;
 import org.onlab.onos.net.intent.IntentService;
 import org.onlab.onos.sdnip.bgp.BgpRouteEntry;
+import org.onlab.onos.sdnip.bgp.BgpSession;
 import org.onlab.onos.sdnip.bgp.BgpSessionManager;
 import org.onlab.onos.sdnip.config.SdnIpConfigReader;
 import org.slf4j.Logger;
@@ -97,6 +98,11 @@
     }
 
     @Override
+    public Collection<BgpSession> getBgpSessions() {
+        return bgpSessionManager.getBgpSessions();
+    }
+
+    @Override
     public Collection<BgpRouteEntry> getBgpRoutes() {
         return bgpSessionManager.getBgpRoutes();
     }
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIpService.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIpService.java
index 936b7e8..8503bee 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIpService.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIpService.java
@@ -18,12 +18,20 @@
 import java.util.Collection;
 
 import org.onlab.onos.sdnip.bgp.BgpRouteEntry;
+import org.onlab.onos.sdnip.bgp.BgpSession;
 
 /**
  * Service interface exported by SDN-IP.
  */
 public interface SdnIpService {
     /**
+     * Gets the BGP sessions.
+     *
+     * @return the BGP sessions
+     */
+    public Collection<BgpSession> getBgpSessions();
+
+    /**
      * Gets the BGP routes.
      *
      * @return the BGP routes
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpNeighborsListCommand.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpNeighborsListCommand.java
new file mode 100644
index 0000000..d8fe15a
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpNeighborsListCommand.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2014 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.onos.sdnip.cli;
+
+import java.util.Collection;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
+import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.sdnip.SdnIpService;
+import org.onlab.onos.sdnip.bgp.BgpSession;
+
+/**
+ * Command to show the BGP neighbors.
+ */
+@Command(scope = "onos", name = "bgp-neighbors",
+         description = "Lists the BGP neighbors")
+public class BgpNeighborsListCommand extends AbstractShellCommand {
+    @Option(name = "-n", aliases = "--neighbor",
+            description = "BGP neighbor to display information about",
+            required = false, multiValued = false)
+    private String bgpNeighbor;
+
+    private static final String FORMAT_NEIGHBOR_LINE1 =
+        "BGP neighbor is %s, remote AS %d, local AS %d";
+    private static final String FORMAT_NEIGHBOR_LINE2 =
+        "  Remote router ID %s, IP %s, BGP version %d, Hold time %d";
+    private static final String FORMAT_NEIGHBOR_LINE3 =
+        "  Local router ID %s, IP %s, BGP version %d, Hold time %d";
+
+    @Override
+    protected void execute() {
+        SdnIpService service = get(SdnIpService.class);
+        Collection<BgpSession> bgpSessions = service.getBgpSessions();
+
+        if (bgpNeighbor != null) {
+            // Print a single neighbor (if found)
+            BgpSession foundBgpSession = null;
+            for (BgpSession bgpSession : bgpSessions) {
+                if (bgpSession.getRemoteBgpId().toString().equals(bgpNeighbor)) {
+                    foundBgpSession = bgpSession;
+                    break;
+                }
+            }
+            if (foundBgpSession != null) {
+                printNeighbor(foundBgpSession);
+            } else {
+                print("BGP neighbor %s not found", bgpNeighbor);
+            }
+            return;
+        }
+
+        // Print all neighbors
+        printNeighbors(bgpSessions);
+    }
+
+    /**
+     * Prints all BGP neighbors.
+     *
+     * @param bgpSessions the BGP sessions for the neighbors to print
+     */
+    private void printNeighbors(Collection<BgpSession> bgpSessions) {
+        if (outputJson()) {
+            print("%s", json(bgpSessions));
+        } else {
+            for (BgpSession bgpSession : bgpSessions) {
+                printNeighbor(bgpSession);
+            }
+        }
+    }
+
+    /**
+     * Prints a BGP neighbor.
+     *
+     * @param bgpSession the BGP session for the neighbor to print
+     */
+    private void printNeighbor(BgpSession bgpSession) {
+        print(FORMAT_NEIGHBOR_LINE1,
+              bgpSession.getRemoteBgpId().toString(),
+              bgpSession.getRemoteAs(),
+              bgpSession.getLocalAs());
+        print(FORMAT_NEIGHBOR_LINE2,
+              bgpSession.getRemoteBgpId().toString(),
+              bgpSession.getRemoteAddress().toString(),
+              bgpSession.getRemoteBgpVersion(),
+              bgpSession.getRemoteHoldtime());
+        print(FORMAT_NEIGHBOR_LINE3,
+              bgpSession.getLocalBgpId().toString(),
+              bgpSession.getLocalAddress().toString(),
+              bgpSession.getLocalBgpVersion(),
+              bgpSession.getLocalHoldtime());
+    }
+
+    /**
+     * Produces a JSON array of BGP neighbors.
+     *
+     * @param bgpSessions the BGP sessions with the data
+     * @return JSON array with the neighbors
+     */
+    private JsonNode json(Collection<BgpSession> bgpSessions) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+
+        for (BgpSession bgpSession : bgpSessions) {
+            result.add(json(mapper, bgpSession));
+        }
+        return result;
+    }
+
+    /**
+     * Produces JSON object for a BGP neighbor.
+     *
+     * @param mapper the JSON object mapper to use
+     * @param bgpSession the BGP session with the data
+     * @return JSON object for the route
+     */
+    private ObjectNode json(ObjectMapper mapper, BgpSession bgpSession) {
+        ObjectNode result = mapper.createObjectNode();
+
+        result.put("remoteAddress", bgpSession.getRemoteAddress().toString());
+        result.put("remoteBgpVersion", bgpSession.getRemoteBgpVersion());
+        result.put("remoteAs", bgpSession.getRemoteAs());
+        result.put("remoteHoldtime", bgpSession.getRemoteHoldtime());
+        result.put("remoteBgpId", bgpSession.getRemoteBgpId().toString());
+        //
+        result.put("localAddress", bgpSession.getLocalAddress().toString());
+        result.put("localBgpVersion", bgpSession.getLocalBgpVersion());
+        result.put("localAs", bgpSession.getLocalAs());
+        result.put("localHoldtime", bgpSession.getLocalHoldtime());
+        result.put("localBgpId", bgpSession.getLocalBgpId().toString());
+
+        return result;
+    }
+}
diff --git a/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml
index 773ead6..d86f8f4 100644
--- a/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -17,6 +17,9 @@
 
   <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
     <command>
+      <action class="org.onlab.onos.sdnip.cli.BgpNeighborsListCommand"/>
+    </command>
+    <command>
       <action class="org.onlab.onos.sdnip.cli.BgpRoutesListCommand"/>
     </command>
     <command>
diff --git a/core/net/src/main/java/org/onlab/onos/net/topology/impl/DefaultTopologyProvider.java b/core/net/src/main/java/org/onlab/onos/net/topology/impl/DefaultTopologyProvider.java
index 33830aa..ac671da 100644
--- a/core/net/src/main/java/org/onlab/onos/net/topology/impl/DefaultTopologyProvider.java
+++ b/core/net/src/main/java/org/onlab/onos/net/topology/impl/DefaultTopologyProvider.java
@@ -61,8 +61,8 @@
 
     // TODO: make these configurable
     private static final int MAX_EVENTS = 100;
-    private static final int MAX_IDLE_MS = 50;
-    private static final int MAX_BATCH_MS = 200;
+    private static final int MAX_IDLE_MS = 5;
+    private static final int MAX_BATCH_MS = 50;
     private static final int MAX_THREADS = 8;
 
     // FIXME: Replace with a system-wide timer instance;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
index 1e47a00..6de94b4 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
@@ -34,10 +34,10 @@
 import org.onlab.onos.store.cluster.messaging.ClusterMessage;
 import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
-import org.onlab.onos.store.serializers.ClusterMessageSerializer;
 import org.onlab.onos.store.serializers.KryoNamespaces;
 import org.onlab.onos.store.serializers.KryoSerializer;
-import org.onlab.onos.store.serializers.MessageSubjectSerializer;
+import org.onlab.onos.store.serializers.impl.ClusterMessageSerializer;
+import org.onlab.onos.store.serializers.impl.MessageSubjectSerializer;
 import org.onlab.util.KryoNamespace;
 import org.onlab.netty.Endpoint;
 import org.onlab.netty.Message;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
index 19dc3e3..4665411 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
@@ -59,7 +59,7 @@
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
 import org.onlab.onos.store.impl.Timestamped;
 import org.onlab.onos.store.serializers.KryoSerializer;
-import org.onlab.onos.store.serializers.DistributedStoreSerializers;
+import org.onlab.onos.store.serializers.impl.DistributedStoreSerializers;
 import org.onlab.packet.ChassisId;
 import org.onlab.util.KryoNamespace;
 import org.onlab.util.NewConcurrentHashMap;
@@ -230,9 +230,10 @@
         final Timestamped<DeviceDescription> deltaDesc = new Timestamped<>(deviceDescription, newTimestamp);
         final DeviceEvent event;
         final Timestamped<DeviceDescription> mergedDesc;
-        synchronized (getOrCreateDeviceDescriptionsMap(deviceId)) {
+        final Map<ProviderId, DeviceDescriptions> device = getOrCreateDeviceDescriptionsMap(deviceId);
+        synchronized (device) {
             event = createOrUpdateDeviceInternal(providerId, deviceId, deltaDesc);
-            mergedDesc = getOrCreateDeviceDescriptionsMap(deviceId).get(providerId).getDeviceDesc();
+            mergedDesc = device.get(providerId).getDeviceDesc();
         }
         if (event != null) {
             log.info("Notifying peers of a device update topology event for providerId: {} and deviceId: {}",
@@ -252,10 +253,10 @@
                                     Timestamped<DeviceDescription> deltaDesc) {
 
         // Collection of DeviceDescriptions for a Device
-        Map<ProviderId, DeviceDescriptions> providerDescs
+        Map<ProviderId, DeviceDescriptions> device
             = getOrCreateDeviceDescriptionsMap(deviceId);
 
-        synchronized (providerDescs) {
+        synchronized (device) {
             // locking per device
 
             if (isDeviceRemoved(deviceId, deltaDesc.timestamp())) {
@@ -263,7 +264,7 @@
                 return null;
             }
 
-            DeviceDescriptions descs = getOrCreateProviderDeviceDescriptions(providerDescs, providerId, deltaDesc);
+            DeviceDescriptions descs = getOrCreateProviderDeviceDescriptions(device, providerId, deltaDesc);
 
             final Device oldDevice = devices.get(deviceId);
             final Device newDevice;
@@ -272,7 +273,7 @@
                 deltaDesc.isNewer(descs.getDeviceDesc())) {
                 // on new device or valid update
                 descs.putDeviceDesc(deltaDesc);
-                newDevice = composeDevice(deviceId, providerDescs);
+                newDevice = composeDevice(deviceId, device);
             } else {
                 // outdated event, ignored.
                 return null;
@@ -444,9 +445,10 @@
         final List<DeviceEvent> events;
         final Timestamped<List<PortDescription>> merged;
 
-        synchronized (getOrCreateDeviceDescriptionsMap(deviceId)) {
+        final Map<ProviderId, DeviceDescriptions> device = getOrCreateDeviceDescriptionsMap(deviceId);
+        synchronized (device) {
             events = updatePortsInternal(providerId, deviceId, timestampedInput);
-            final DeviceDescriptions descs = getOrCreateDeviceDescriptionsMap(deviceId).get(providerId);
+            final DeviceDescriptions descs = device.get(providerId);
             List<PortDescription> mergedList =
                     FluentIterable.from(portDescriptions)
                 .transform(new Function<PortDescription, PortDescription>() {
@@ -632,9 +634,10 @@
             = new Timestamped<>(portDescription, newTimestamp);
         final DeviceEvent event;
         final Timestamped<PortDescription> mergedDesc;
-        synchronized (getOrCreateDeviceDescriptionsMap(deviceId)) {
+        final Map<ProviderId, DeviceDescriptions> device = getOrCreateDeviceDescriptionsMap(deviceId);
+        synchronized (device) {
             event = updatePortStatusInternal(providerId, deviceId, deltaDesc);
-            mergedDesc = getOrCreateDeviceDescriptionsMap(deviceId).get(providerId)
+            mergedDesc = device.get(providerId)
                             .getPortDesc(portDescription.portNumber());
         }
         if (event != null) {
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
index 81f2bfd..f65da3f 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
@@ -75,9 +75,9 @@
 import org.onlab.onos.store.hz.AbstractHazelcastStore;
 import org.onlab.onos.store.hz.SMap;
 import org.onlab.onos.store.serializers.DecodeTo;
-import org.onlab.onos.store.serializers.DistributedStoreSerializers;
 import org.onlab.onos.store.serializers.KryoSerializer;
 import org.onlab.onos.store.serializers.StoreSerializer;
+import org.onlab.onos.store.serializers.impl.DistributedStoreSerializers;
 import org.onlab.util.KryoNamespace;
 import org.slf4j.Logger;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
index 30a73e0..5353a91 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
@@ -67,8 +67,8 @@
 import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
 import org.onlab.onos.store.impl.Timestamped;
-import org.onlab.onos.store.serializers.DistributedStoreSerializers;
 import org.onlab.onos.store.serializers.KryoSerializer;
+import org.onlab.onos.store.serializers.impl.DistributedStoreSerializers;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
index 351581d..e82742a 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
@@ -55,8 +55,8 @@
 import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
 import org.onlab.onos.store.impl.Timestamped;
-import org.onlab.onos.store.serializers.DistributedStoreSerializers;
 import org.onlab.onos.store.serializers.KryoSerializer;
+import org.onlab.onos.store.serializers.impl.DistributedStoreSerializers;
 import org.onlab.util.KryoNamespace;
 import org.slf4j.Logger;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/resource/impl/DistributedLinkResourceStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/resource/impl/DistributedLinkResourceStore.java
index ecccd2b..d05620d 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/resource/impl/DistributedLinkResourceStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/resource/impl/DistributedLinkResourceStore.java
@@ -73,7 +73,7 @@
      * @param link the target link
      * @return free resources
      */
-    private Set<ResourceAllocation> readOriginalFreeResources(Link link) {
+    private synchronized Set<ResourceAllocation> readOriginalFreeResources(Link link) {
         // TODO read capacity and lambda resources from topology
         Set<ResourceAllocation> allocations = new HashSet<>();
         for (int i = 1; i <= 100; i++) {
@@ -92,7 +92,7 @@
      *         {@link org.onlab.onos.net.resource.BandwidthResourceAllocation} object with 0 bandwidth
      *
      */
-    private BandwidthResourceAllocation getBandwidth(Set<ResourceAllocation> freeRes) {
+    private synchronized BandwidthResourceAllocation getBandwidth(Set<ResourceAllocation> freeRes) {
         for (ResourceAllocation res : freeRes) {
             if (res.type() == ResourceType.BANDWIDTH) {
                 return (BandwidthResourceAllocation) res;
@@ -107,7 +107,7 @@
      * @param link the target link
      * @param allocations the resources to be subtracted
      */
-    private void subtractFreeResources(Link link, LinkResourceAllocations allocations) {
+    private synchronized void subtractFreeResources(Link link, LinkResourceAllocations allocations) {
         // TODO Use lock or version for updating freeResources.
         checkNotNull(link);
         Set<ResourceAllocation> freeRes = new HashSet<>(getFreeResources(link));
@@ -141,7 +141,7 @@
      * @param link the target link
      * @param allocations the resources to be added
      */
-    private void addFreeResources(Link link, LinkResourceAllocations allocations) {
+    private synchronized void addFreeResources(Link link, LinkResourceAllocations allocations) {
         // TODO Use lock or version for updating freeResources.
         Set<ResourceAllocation> freeRes = new HashSet<>(getFreeResources(link));
         Set<ResourceAllocation> addRes = allocations.getResourceAllocation(link);
@@ -167,7 +167,7 @@
     }
 
     @Override
-    public Set<ResourceAllocation> getFreeResources(Link link) {
+    public synchronized Set<ResourceAllocation> getFreeResources(Link link) {
         checkNotNull(link);
         Set<ResourceAllocation> freeRes = freeResources.get(link);
         if (freeRes == null) {
@@ -178,7 +178,7 @@
     }
 
     @Override
-    public void allocateResources(LinkResourceAllocations allocations) {
+    public synchronized void allocateResources(LinkResourceAllocations allocations) {
         checkNotNull(allocations);
         linkResourceAllocationsMap.put(allocations.intendId(), allocations);
         for (Link link : allocations.links()) {
@@ -193,7 +193,7 @@
     }
 
     @Override
-    public void releaseResources(LinkResourceAllocations allocations) {
+    public synchronized void releaseResources(LinkResourceAllocations allocations) {
         checkNotNull(allocations);
         linkResourceAllocationsMap.remove(allocations.intendId());
         for (Link link : allocations.links()) {
@@ -209,13 +209,13 @@
     }
 
     @Override
-    public LinkResourceAllocations getAllocations(IntentId intentId) {
+    public synchronized LinkResourceAllocations getAllocations(IntentId intentId) {
         checkNotNull(intentId);
         return linkResourceAllocationsMap.get(intentId);
     }
 
     @Override
-    public Iterable<LinkResourceAllocations> getAllocations(Link link) {
+    public synchronized Iterable<LinkResourceAllocations> getAllocations(Link link) {
         checkNotNull(link);
         Set<LinkResourceAllocations> result = allocatedResources.get(link);
         if (result == null) {
@@ -225,7 +225,7 @@
     }
 
     @Override
-    public Iterable<LinkResourceAllocations> getAllocations() {
+    public synchronized Iterable<LinkResourceAllocations> getAllocations() {
         return Collections.unmodifiableCollection(linkResourceAllocationsMap.values());
     }
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/ClusterMessageSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/ClusterMessageSerializer.java
similarity index 97%
rename from core/store/dist/src/main/java/org/onlab/onos/store/serializers/ClusterMessageSerializer.java
rename to core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/ClusterMessageSerializer.java
index 9cc7f8a..d3f31bb 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/ClusterMessageSerializer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/ClusterMessageSerializer.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onlab.onos.store.serializers;
+package org.onlab.onos.store.serializers.impl;
 
 import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.store.cluster.messaging.ClusterMessage;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/DistributedStoreSerializers.java b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/DistributedStoreSerializers.java
similarity index 92%
rename from core/store/dist/src/main/java/org/onlab/onos/store/serializers/DistributedStoreSerializers.java
rename to core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/DistributedStoreSerializers.java
index d2de5ad..2de47bd 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/DistributedStoreSerializers.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/DistributedStoreSerializers.java
@@ -13,11 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onlab.onos.store.serializers;
+package org.onlab.onos.store.serializers.impl;
 
 import org.onlab.onos.store.impl.MastershipBasedTimestamp;
 import org.onlab.onos.store.impl.Timestamped;
 import org.onlab.onos.store.impl.WallClockTimestamp;
+import org.onlab.onos.store.serializers.KryoNamespaces;
 import org.onlab.util.KryoNamespace;
 
 public final class DistributedStoreSerializers {
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/MastershipBasedTimestampSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/MastershipBasedTimestampSerializer.java
similarity index 97%
rename from core/store/dist/src/main/java/org/onlab/onos/store/serializers/MastershipBasedTimestampSerializer.java
rename to core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/MastershipBasedTimestampSerializer.java
index 4221d57..83bff3c 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/MastershipBasedTimestampSerializer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/MastershipBasedTimestampSerializer.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onlab.onos.store.serializers;
+package org.onlab.onos.store.serializers.impl;
 
 import org.onlab.onos.store.impl.MastershipBasedTimestamp;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/MessageSubjectSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/MessageSubjectSerializer.java
similarity index 96%
rename from core/store/dist/src/main/java/org/onlab/onos/store/serializers/MessageSubjectSerializer.java
rename to core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/MessageSubjectSerializer.java
index e94d302..1173165 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/MessageSubjectSerializer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/impl/MessageSubjectSerializer.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onlab.onos.store.serializers;
+package org.onlab.onos.store.serializers.impl;
 
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolClient.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolClient.java
index 976fbcf..bb6bfcf 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolClient.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolClient.java
@@ -85,7 +85,7 @@
         return CompletableFuture.completedFuture(null);
     }
 
-    public <I> MessageSubject messageType(I input) {
+    private <I> MessageSubject messageType(I input) {
         Class<?> clazz = input.getClass();
         if (clazz.equals(PollRequest.class)) {
             return ClusterMessagingProtocol.COPYCAT_POLL;
@@ -117,7 +117,7 @@
             this.request = request;
             this.message =
                     new ClusterMessage(
-                            null,
+                            null, // FIXME fill in proper sender
                             messageType(request),
                             ClusterMessagingProtocol.SERIALIZER.encode(request));
             this.future = future;
@@ -132,19 +132,20 @@
                 future.complete(ClusterMessagingProtocol.SERIALIZER.decode(response));
 
             } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) {
-                if (message.subject().equals(ClusterMessagingProtocol.COPYCAT_SYNC) ||
-                        message.subject().equals(ClusterMessagingProtocol.COPYCAT_PING)) {
-                    log.warn("{} Request to {} failed. Will retry in {} ms",
-                             message.subject(), remoteNode, RETRY_INTERVAL_MILLIS);
-                    THREAD_POOL.schedule(
-                            this,
-                            RETRY_INTERVAL_MILLIS,
-                            TimeUnit.MILLISECONDS);
-                } else {
+//                if (message.subject().equals(ClusterMessagingProtocol.COPYCAT_SYNC) ||
+//                        message.subject().equals(ClusterMessagingProtocol.COPYCAT_PING)) {
+//                    log.warn("{} Request to {} failed. Will retry in {} ms",
+//                             message.subject(), remoteNode, RETRY_INTERVAL_MILLIS);
+//                    THREAD_POOL.schedule(
+//                            this,
+//                            RETRY_INTERVAL_MILLIS,
+//                            TimeUnit.MILLISECONDS);
+//                } else {
                     log.warn("RPCTask for {} failed.", request, e);
                     future.completeExceptionally(e);
-                }
+//                }
             } catch (Exception e) {
+                log.warn("RPCTask for {} terribly failed.", request, e);
                 future.completeExceptionally(e);
             }
         }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolServer.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolServer.java
index e2c06c7..a5f207e 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolServer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolServer.java
@@ -67,14 +67,36 @@
         @Override
         public void handle(ClusterMessage message) {
             T request = ClusterMessagingProtocol.SERIALIZER.decode(message.payload());
+            if (handler == null) {
+                // there is a slight window of time during state transition,
+                // where handler becomes null
+                for (int i = 0; i < 10; ++i) {
+                    if (handler != null) {
+                        break;
+                    }
+                    try {
+                        Thread.sleep(1);
+                    } catch (InterruptedException e) {
+                        log.trace("Exception", e);
+                    }
+                }
+                if (handler == null) {
+                    log.error("There was no handler for registered!");
+                    return;
+                }
+            }
             if (request.getClass().equals(PingRequest.class)) {
-                handler.ping((PingRequest) request).whenComplete(new PostExecutionTask<PingResponse>(message));
+                handler.ping((PingRequest) request)
+                    .whenComplete(new PostExecutionTask<PingResponse>(message));
             } else if (request.getClass().equals(PollRequest.class)) {
-                handler.poll((PollRequest) request).whenComplete(new PostExecutionTask<PollResponse>(message));
+                handler.poll((PollRequest) request)
+                    .whenComplete(new PostExecutionTask<PollResponse>(message));
             } else if (request.getClass().equals(SyncRequest.class)) {
-                handler.sync((SyncRequest) request).whenComplete(new PostExecutionTask<SyncResponse>(message));
+                handler.sync((SyncRequest) request)
+                    .whenComplete(new PostExecutionTask<SyncResponse>(message));
             } else if (request.getClass().equals(SubmitRequest.class)) {
-                handler.submit((SubmitRequest) request).whenComplete(new PostExecutionTask<SubmitResponse>(message));
+                handler.submit((SubmitRequest) request)
+                    .whenComplete(new PostExecutionTask<SubmitResponse>(message));
             } else {
                 throw new IllegalStateException("Unknown request type: " + request.getClass().getName());
             }
@@ -94,6 +116,7 @@
                     log.error("Processing for " + message.subject() + " failed.", t);
                 } else {
                     try {
+                        log.trace("responding to {}", message.subject());
                         message.respond(ClusterMessagingProtocol.SERIALIZER.encode(response));
                     } catch (Exception e) {
                         log.error("Failed to respond to " + response.getClass().getName(), e);
diff --git a/core/store/dist/src/test/java/org/onlab/onos/store/impl/MastershipBasedTimestampTest.java b/core/store/dist/src/test/java/org/onlab/onos/store/impl/MastershipBasedTimestampTest.java
index 0060d44..e1da794 100644
--- a/core/store/dist/src/test/java/org/onlab/onos/store/impl/MastershipBasedTimestampTest.java
+++ b/core/store/dist/src/test/java/org/onlab/onos/store/impl/MastershipBasedTimestampTest.java
@@ -21,7 +21,7 @@
 
 import org.junit.Test;
 import org.onlab.onos.store.Timestamp;
-import org.onlab.onos.store.serializers.MastershipBasedTimestampSerializer;
+import org.onlab.onos.store.serializers.impl.MastershipBasedTimestampSerializer;
 import org.onlab.util.KryoNamespace;
 
 import com.google.common.testing.EqualsTester;
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkResourceStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkResourceStore.java
index 5369375..78cd341 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkResourceStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkResourceStore.java
@@ -73,7 +73,7 @@
      * @param link the target link
      * @return free resources
      */
-    private Set<ResourceAllocation> readOriginalFreeResources(Link link) {
+    private synchronized Set<ResourceAllocation> readOriginalFreeResources(Link link) {
         // TODO read capacity and lambda resources from topology
         Set<ResourceAllocation> allocations = new HashSet<>();
         for (int i = 1; i <= 100; i++) {
@@ -92,7 +92,7 @@
      *         {@link BandwidthResourceAllocation} object with 0 bandwidth
      *
      */
-    private BandwidthResourceAllocation getBandwidth(Set<ResourceAllocation> freeRes) {
+    private synchronized BandwidthResourceAllocation getBandwidth(Set<ResourceAllocation> freeRes) {
         for (ResourceAllocation res : freeRes) {
             if (res.type() == ResourceType.BANDWIDTH) {
                 return (BandwidthResourceAllocation) res;
@@ -107,7 +107,7 @@
      * @param link the target link
      * @param allocations the resources to be subtracted
      */
-    private void subtractFreeResources(Link link, LinkResourceAllocations allocations) {
+    private synchronized void subtractFreeResources(Link link, LinkResourceAllocations allocations) {
         // TODO Use lock or version for updating freeResources.
         checkNotNull(link);
         Set<ResourceAllocation> freeRes = new HashSet<>(getFreeResources(link));
@@ -141,7 +141,7 @@
      * @param link the target link
      * @param allocations the resources to be added
      */
-    private void addFreeResources(Link link, LinkResourceAllocations allocations) {
+    private synchronized void addFreeResources(Link link, LinkResourceAllocations allocations) {
         // TODO Use lock or version for updating freeResources.
         Set<ResourceAllocation> freeRes = new HashSet<>(getFreeResources(link));
         Set<ResourceAllocation> addRes = allocations.getResourceAllocation(link);
@@ -167,7 +167,7 @@
     }
 
     @Override
-    public Set<ResourceAllocation> getFreeResources(Link link) {
+    public synchronized Set<ResourceAllocation> getFreeResources(Link link) {
         checkNotNull(link);
         Set<ResourceAllocation> freeRes = freeResources.get(link);
         if (freeRes == null) {
@@ -178,7 +178,7 @@
     }
 
     @Override
-    public void allocateResources(LinkResourceAllocations allocations) {
+    public synchronized void allocateResources(LinkResourceAllocations allocations) {
         checkNotNull(allocations);
         linkResourceAllocationsMap.put(allocations.intendId(), allocations);
         for (Link link : allocations.links()) {
@@ -193,7 +193,7 @@
     }
 
     @Override
-    public void releaseResources(LinkResourceAllocations allocations) {
+    public synchronized void releaseResources(LinkResourceAllocations allocations) {
         checkNotNull(allocations);
         linkResourceAllocationsMap.remove(allocations.intendId());
         for (Link link : allocations.links()) {
@@ -209,13 +209,13 @@
     }
 
     @Override
-    public LinkResourceAllocations getAllocations(IntentId intentId) {
+    public synchronized LinkResourceAllocations getAllocations(IntentId intentId) {
         checkNotNull(intentId);
         return linkResourceAllocationsMap.get(intentId);
     }
 
     @Override
-    public Iterable<LinkResourceAllocations> getAllocations(Link link) {
+    public synchronized Iterable<LinkResourceAllocations> getAllocations(Link link) {
         checkNotNull(link);
         Set<LinkResourceAllocations> result = allocatedResources.get(link);
         if (result == null) {
@@ -225,7 +225,7 @@
     }
 
     @Override
-    public Iterable<LinkResourceAllocations> getAllocations() {
+    public synchronized Iterable<LinkResourceAllocations> getAllocations() {
         return Collections.unmodifiableCollection(linkResourceAllocationsMap.values());
     }
 
diff --git a/utils/netty/src/main/java/org/onlab/netty/NettyMessagingService.java b/utils/netty/src/main/java/org/onlab/netty/NettyMessagingService.java
index 4a71265..771300f 100644
--- a/utils/netty/src/main/java/org/onlab/netty/NettyMessagingService.java
+++ b/utils/netty/src/main/java/org/onlab/netty/NettyMessagingService.java
@@ -316,7 +316,11 @@
                 return;
             }
             MessageHandler handler = NettyMessagingService.this.getMessageHandler(type);
-            handler.handle(message);
+            if (handler != null) {
+                handler.handle(message);
+            } else {
+                log.debug("No handler registered for {}", type);
+            }
         }
 
         @Override
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
index 7828043..31c3f84 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
@@ -15,8 +15,20 @@
  */
 package org.onlab.onos.gui;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.eclipse.jetty.websocket.WebSocket;
+import org.onlab.onos.event.Event;
+import org.onlab.onos.net.Annotations;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.device.DeviceEvent;
 import org.onlab.onos.net.device.DeviceService;
+import org.onlab.onos.net.link.LinkEvent;
 import org.onlab.onos.net.topology.Topology;
 import org.onlab.onos.net.topology.TopologyEdge;
 import org.onlab.onos.net.topology.TopologyEvent;
@@ -27,6 +39,15 @@
 import org.onlab.osgi.ServiceDirectory;
 
 import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_ADDED;
+import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_REMOVED;
+import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED;
+import static org.onlab.onos.net.link.LinkEvent.Type.LINK_REMOVED;
 
 /**
  * Web socket capable of interacting with the GUI topology view.
@@ -37,8 +58,16 @@
     private final TopologyService topologyService;
     private final DeviceService deviceService;
 
+    private final ObjectMapper mapper = new ObjectMapper();
+
     private Connection connection;
 
+    // TODO: extract into an external & durable state; good enough for now and demo
+    private static Map<String, ObjectNode> metaUi = new HashMap<>();
+
+    private static final String COMPACT = "%s/%s-%s/%s";
+
+
     /**
      * Creates a new web-socket for serving data to GUI topology view.
      *
@@ -58,22 +87,19 @@
         if (topologyService != null && deviceService != null) {
             topologyService.addListener(this);
 
-            sendMessage("Yo!!!");
-
             Topology topology = topologyService.currentTopology();
             TopologyGraph graph = topologyService.getGraph(topology);
             for (TopologyVertex vertex : graph.getVertexes()) {
-                sendMessage(deviceService.getDevice(vertex.deviceId()).toString());
+                sendMessage(message(new DeviceEvent(DEVICE_ADDED,
+                                                    deviceService.getDevice(vertex.deviceId()))));
             }
 
             for (TopologyEdge edge : graph.getEdges()) {
-                sendMessage(edge.link().toString());
+                sendMessage(message(new LinkEvent(LINK_ADDED, edge.link())));
             }
 
-            sendMessage("That's what we're starting with...");
-
         } else {
-            sendMessage("No topology service!!!");
+            sendMessage(message("error", "No topology service!!!"));
         }
     }
 
@@ -87,10 +113,57 @@
 
     @Override
     public void onMessage(String data) {
-        System.out.println("Received: " + data);
+        try {
+            ObjectNode event = (ObjectNode) mapper.reader().readTree(data);
+            String type = event.path("event").asText("unknown");
+            ObjectNode payload = (ObjectNode) event.path("payload");
+
+            switch (type) {
+                case "updateMeta":
+                    metaUi.put(payload.path("id").asText(), payload);
+                    break;
+                case "requestPath":
+                    findPath(deviceId(payload.path("one").asText()),
+                             deviceId(payload.path("two").asText()));
+                default:
+                    break;
+            }
+        } catch (IOException e) {
+            System.out.println("Received: " + data);
+        }
     }
 
-    public void sendMessage(String data) {
+    private void findPath(DeviceId one, DeviceId two) {
+        Set<Path> paths = topologyService.getPaths(topologyService.currentTopology(),
+                                                   one, two);
+        if (!paths.isEmpty()) {
+            ObjectNode payload = mapper.createObjectNode();
+            ArrayNode links = mapper.createArrayNode();
+
+            Path path = paths.iterator().next();
+            for (Link link : path.links()) {
+                links.add(compactLinkString(link));
+            }
+
+            payload.set("links", links);
+            sendMessage(envelope("showPath", payload));
+        }
+        // TODO: when no path, send a message to the client
+    }
+
+    /**
+     * Returns a compact string representing the given link.
+     *
+     * @param link infrastructure link
+     * @return formatted link string
+     */
+    public static String compactLinkString(Link link) {
+        return String.format(COMPACT, link.src().deviceId(), link.src().port(),
+                             link.dst().deviceId(), link.dst().port());
+    }
+
+
+    private void sendMessage(String data) {
         try {
             connection.sendMessage(data);
         } catch (IOException e) {
@@ -98,9 +171,84 @@
         }
     }
 
+    // Produces a link event message to the client.
+    private String message(DeviceEvent event) {
+        Device device = event.subject();
+        ObjectNode payload = mapper.createObjectNode()
+                .put("id", device.id().toString())
+                .put("type", device.type().toString().toLowerCase())
+                .put("online", deviceService.isAvailable(device.id()));
+
+        // Generate labels: id, chassis id, no-label, optional-name
+        ArrayNode labels = mapper.createArrayNode();
+        labels.add(device.id().toString());
+        labels.add(device.chassisId().toString());
+        labels.add(" "); // compact no-label view
+        labels.add(device.annotations().value("name"));
+
+        // Add labels, props and stuff the payload into envelope.
+        payload.set("labels", labels);
+        payload.set("props", props(device.annotations()));
+
+        ObjectNode meta = metaUi.get(device.id().toString());
+        if (meta != null) {
+            payload.set("metaUi", meta);
+        }
+
+        String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
+                ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
+        return envelope(type, payload);
+    }
+
+    // Produces a link event message to the client.
+    private String message(LinkEvent event) {
+        Link link = event.subject();
+        ObjectNode payload = mapper.createObjectNode()
+                .put("type", link.type().toString().toLowerCase())
+                .put("linkWidth", 2)
+                .put("src", link.src().deviceId().toString())
+                .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" : "removeLink");
+        return envelope(type, payload);
+    }
+
+    // Produces JSON structure from annotations.
+    private JsonNode props(Annotations annotations) {
+        ObjectNode props = mapper.createObjectNode();
+        for (String key : annotations.keys()) {
+            props.put(key, annotations.value(key));
+        }
+        return props;
+    }
+
+    // Produces a log message event bound to the client.
+    private String message(String severity, String message) {
+        return envelope("message",
+                        mapper.createObjectNode()
+                                .put("severity", severity)
+                                .put("message", message));
+    }
+
+    // Puts the payload into an envelope and returns it.
+    private String envelope(String type, ObjectNode payload) {
+        ObjectNode event = mapper.createObjectNode();
+        event.put("event", type);
+        event.set("payload", payload);
+        return event.toString();
+    }
+
     @Override
     public void event(TopologyEvent event) {
-        sendMessage(event.toString());
+        for (Event reason : event.reasons()) {
+            if (reason instanceof DeviceEvent) {
+                sendMessage(message((DeviceEvent) reason));
+            } else if (reason instanceof LinkEvent) {
+                sendMessage(message((LinkEvent) reason));
+            }
+        }
     }
 }
 
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 235c1d7..6168271 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -72,9 +72,9 @@
     <!-- Initialize the UI...-->
     <script type="text/javascript">
         var ONOS = $.onos({
-            comment: "configuration options",
+            comment: 'configuration options',
+            theme: 'light',
             startVid: 'topo',
-//            startVid: 'sampleKeys',
             trace: false
         });
     </script>
diff --git a/web/gui/src/main/webapp/json/intent/ev_1_ui.json b/web/gui/src/main/webapp/json/intent/ev_1_ui.json
new file mode 100644
index 0000000..962fcaa
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_1_ui.json
@@ -0,0 +1,8 @@
+{
+  "event": "addHostIntent",
+  "sid": 1,
+  "payload": {
+    "one": "hostOne",
+    "two": "hostTwo"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_2_onos.json b/web/gui/src/main/webapp/json/intent/ev_2_onos.json
new file mode 100644
index 0000000..2b3bbe5
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_2_onos.json
@@ -0,0 +1,11 @@
+{
+  "event": "showPath",
+  "sid": 1,
+  "payload": {
+    "intentId": "0x1234",
+    "path": {
+      "links": [ "1-2", "2-3" ],
+      "traffic": false
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_3_ui.json b/web/gui/src/main/webapp/json/intent/ev_3_ui.json
new file mode 100644
index 0000000..3151c8d
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_3_ui.json
@@ -0,0 +1,7 @@
+{
+  "event": "monitorIntent",
+  "sid": 2,
+  "payload": {
+    "intentId": "0x1234"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_4_onos.json b/web/gui/src/main/webapp/json/intent/ev_4_onos.json
new file mode 100644
index 0000000..1a6632e
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_4_onos.json
@@ -0,0 +1,13 @@
+{
+  "event": "showPath",
+  "sid": 2,
+  "payload": {
+    "intentId": "0x1234",
+    "path": {
+      "links": [ "1-2", "2-3" ],
+      "traffic": true,
+      "srcLabel": "567 Mb",
+      "dstLabel": "6 Mb"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_5_onos.json b/web/gui/src/main/webapp/json/intent/ev_5_onos.json
new file mode 100644
index 0000000..7fd0ee4
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_5_onos.json
@@ -0,0 +1,13 @@
+{
+  "event": "showPath",
+  "sid": 2,
+  "payload": {
+    "intentId": "0x1234",
+    "path": {
+      "links": [ "1-2", "2-3" ],
+      "traffic": true,
+      "srcLabel": "967 Mb",
+      "dstLabel": "65 Mb"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_6_onos.json b/web/gui/src/main/webapp/json/intent/ev_6_onos.json
new file mode 100644
index 0000000..be3925e
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_6_onos.json
@@ -0,0 +1,11 @@
+{
+  "event": "showPath",
+  "sid": 2,
+  "payload": {
+    "intentId": "0x1234",
+    "path": {
+      "links": [ "1-2", "2-3" ],
+      "traffic": false
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/intent/ev_7_ui.json b/web/gui/src/main/webapp/json/intent/ev_7_ui.json
new file mode 100644
index 0000000..158476e
--- /dev/null
+++ b/web/gui/src/main/webapp/json/intent/ev_7_ui.json
@@ -0,0 +1,7 @@
+{
+  "event": "cancelMonitorIntent",
+  "sid": 3,
+  "payload": {
+    "intentId": "0x1234"
+  }
+}
diff --git a/web/gui/src/main/webapp/mast2.css b/web/gui/src/main/webapp/mast2.css
index 7f094b3..57fede4 100644
--- a/web/gui/src/main/webapp/mast2.css
+++ b/web/gui/src/main/webapp/mast2.css
@@ -23,8 +23,15 @@
 #mast {
     height: 36px;
     padding: 4px;
-    background-color: #bbb;
     vertical-align: baseline;
+}
+
+.light #mast {
+    background-color: #bbb;
+    box-shadow: 0px 2px 8px #777;
+}
+.dark #mast {
+    background-color: #444;
     box-shadow: 0px 2px 8px #777;
 }
 
@@ -35,12 +42,18 @@
 }
 
 #mast span.title {
-    color: #369;
     font-size: 14pt;
     font-style: italic;
     vertical-align: 12px;
 }
 
+.light #mast span.title {
+    color: #369;
+}
+.dark #mast span.title {
+    color: #78a;
+}
+
 #mast span.right {
     padding-top: 8px;
     padding-right: 16px;
@@ -50,16 +63,31 @@
 #mast span.radio {
     font-size: 10pt;
     margin: 4px 2px;
-    border: 1px dotted #222;
     padding: 1px 6px;
-    color: #eee;
     cursor: pointer;
 }
 
+.light #mast span.radio {
+    border: 1px dotted #222;
+    color: #eee;
+}
+.dark #mast span.radio {
+    border: 1px dotted #bbb;
+    color: #888;
+}
+
 #mast span.radio.active {
+    padding: 1px 6px;
+    font-weight: bold;
+}
+
+.light #mast span.radio.active {
     background-color: #bbb;
     border: 1px solid #eee;
-    padding: 1px 6px;
     color: #666;
-    font-weight: bold;
+}
+.dark #mast span.radio.active {
+    background-color: #222;
+    border: 1px solid #eee;
+    color: #aaf;
 }
diff --git a/web/gui/src/main/webapp/onos2.css b/web/gui/src/main/webapp/onos2.css
index 748cc97..2073acf 100644
--- a/web/gui/src/main/webapp/onos2.css
+++ b/web/gui/src/main/webapp/onos2.css
@@ -32,7 +32,7 @@
     display: block;
 }
 
-div#alerts {
+#alerts {
     display: none;
     position: absolute;
     z-index: 2000;
@@ -45,21 +45,28 @@
     box-shadow: 4px 6px 12px #777;
 }
 
-div#alerts pre {
+#alerts pre {
     margin: 0.2em 6px;
 }
 
-div#alerts span.close {
+#alerts span.close {
     color: #6af;
     float: right;
     right: 2px;
     cursor: pointer;
 }
 
-div#alerts span.close:hover {
+#alerts span.close:hover {
     color: #fff;
 }
 
+#alerts p.footnote {
+    text-align: right;
+    font-size: 8pt;
+    margin: 8px 0 0 0;
+    color: #66d;
+}
+
 /*
  * ==============================================================
  * END OF NEW ONOS.JS file
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 31d89fa..4aeb23e 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -37,23 +37,34 @@
 
         var defaultOptions = {
             trace: false,
+            theme: 'light',
             startVid: defaultVid
         };
 
         // compute runtime settings
         var settings = $.extend({}, defaultOptions, options);
 
+        // set the selected theme
+        d3.select('body').classed(settings.theme, true);
+
         // internal state
         var views = {},
             current = {
                 view: null,
-                ctx: ''
+                ctx: '',
+                theme: settings.theme
             },
             built = false,
             errorCount = 0,
             keyHandler = {
-                fn: null,
-                map: {}
+                globalKeys: {},
+                maskedKeys: {},
+                viewKeys: {},
+                viewFn: null
+            },
+            alerts = {
+                open: false,
+                count: 0
             };
 
         // DOM elements etc.
@@ -240,8 +251,8 @@
 
                 // detach radio buttons, key handlers, etc.
                 $('#mastRadio').children().detach();
-                keyHandler.fn = null;
-                keyHandler.map = {};
+                keyHandler.viewKeys = {};
+                keyHandler.viewFn = null;
             }
 
             // cache new view and context
@@ -322,20 +333,74 @@
             $mastRadio.node().appendChild(btnG.node());
         }
 
+        function setupGlobalKeys() {
+            keyHandler.globalKeys = {
+                esc: escapeKey,
+                T: toggleTheme
+            };
+            // Masked keys are global key handlers that always return true.
+            // That is, the view will never see the event for that key.
+            keyHandler.maskedKeys = {
+                T: true
+            };
+        }
+
+        function escapeKey(view, key, code, ev) {
+            if (alerts.open) {
+                closeAlerts();
+                return true;
+            }
+            return false;
+        }
+
+        function toggleTheme(view, key, code, ev) {
+            var body = d3.select('body');
+            current.theme = (current.theme === 'light') ? 'dark' : 'light';
+            body.classed('light dark', false);
+            body.classed(current.theme, true);
+            return true;
+        }
+
         function setKeyBindings(keyArg) {
+            var viewKeys,
+                masked = [];
+
             if ($.isFunction(keyArg)) {
                 // set general key handler callback
-                keyHandler.fn = keyArg;
+                keyHandler.viewFn = keyArg;
             } else {
                 // set specific key filter map
-                keyHandler.map = keyArg;
+                viewKeys = d3.map(keyArg).keys();
+                viewKeys.forEach(function (key) {
+                    if (keyHandler.maskedKeys[key]) {
+                        masked.push('  Key "' + key + '" is reserved');
+                    }
+                });
+
+                if (masked.length) {
+                    doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
+                }
+                keyHandler.viewKeys = keyArg;
             }
         }
 
-        var alerts = {
-            open: false,
-            count: 0
-        };
+        function keyIn() {
+            var event = d3.event,
+                keyCode = event.keyCode,
+                key = whatKey(keyCode),
+                gcb = isF(keyHandler.globalKeys[key]),
+                vcb = isF(keyHandler.viewKeys[key]) || isF(keyHandler.viewFn);
+
+            // global callback?
+            if (gcb && gcb(current.view.token(), key, keyCode, event)) {
+                // if the event was 'handled', we are done
+                return;
+            }
+            // otherwise, let the view callback have a shot
+            if (vcb) {
+                vcb(current.view.token(), key, keyCode, event);
+            }
+        }
 
         function createAlerts() {
             var al = d3.select('#alerts')
@@ -345,15 +410,16 @@
                 .text('X')
                 .on('click', closeAlerts);
             al.append('pre');
+            al.append('p').attr('class', 'footnote')
+                .text('Press ESCAPE to close');
             alerts.open = true;
             alerts.count = 0;
         }
 
         function closeAlerts() {
             d3.select('#alerts')
-                .style('display', 'none');
-            d3.select('#alerts span').remove();
-            d3.select('#alerts pre').remove();
+                .style('display', 'none')
+                .html('');
             alerts.open = false;
         }
 
@@ -384,17 +450,6 @@
             addAlert(msg);
         }
 
-        function keyIn() {
-            var event = d3.event,
-                keyCode = event.keyCode,
-                key = whatKey(keyCode),
-                cb = isF(keyHandler.map[key]) || isF(keyHandler.fn);
-
-            if (cb) {
-                cb(current.view.token(), key, keyCode, event);
-            }
-        }
-
         function resize(e) {
             d3.selectAll('.onosView').call(setViewDimensions);
             // allow current view to react to resize event...
@@ -683,6 +738,7 @@
             $(window).on('resize', resize);
 
             d3.select('body').on('keydown', keyIn);
+            setupGlobalKeys();
 
             // Invoke hashchange callback to navigate to content
             // indicated by the window location hash.
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index e7444c9..acd2464 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -28,7 +28,7 @@
 
     // configuration data
     var config = {
-        useLiveData: false,
+        useLiveData: true,
         debugOn: false,
         debug: {
             showNodeXY: true,
@@ -113,27 +113,31 @@
 
     // key bindings
     var keyDispatch = {
-        space: injectTestEvent,     // TODO: remove (testing only)
-        S: injectStartupEvents,     // TODO: remove (testing only)
-        A: testAlert,               // TODO: remove (testing only)
         M: testMe,                  // TODO: remove (testing only)
+        S: injectStartupEvents,     // TODO: remove (testing only)
+        space: injectTestEvent,     // TODO: remove (testing only)
 
-        B: toggleBg,
-        G: toggleLayout,
+        B: toggleBg,                // TODO: do we really need this?
         L: cycleLabels,
         P: togglePorts,
-        U: unpin
+        U: unpin,
+
+        X: requestPath
     };
 
     // state variables
     var network = {
+            view: null,     // view token reference
             nodes: [],
             links: [],
             lookup: {}
         },
         webSock,
         labelIdx = 0,
-        selected = {},
+
+        selectOrder = [],
+        selections = {},
+
         highlighted = null,
         hovered = null,
         viewMode = 'showAll',
@@ -167,19 +171,19 @@
     // ==============================
     // Key Callbacks
 
-    function testAlert(view) {
-        alertNumber++;
-        view.alert("Test me! -- " + alertNumber);
-    }
-
     function testMe(view) {
+        view.alert('test');
     }
 
     function injectTestEvent(view) {
+        if (config.useLiveData) {
+            view.alert("Sorry, currently using live data..");
+            return;
+        }
+
         eventNumber++;
         var eventUrl = eventPrefix + eventNumber + '.json';
 
-        console.log('Fetching JSON: ' + eventUrl);
         d3.json(eventUrl, function(err, data) {
             if (err) {
                 view.dataLoadError(err, eventUrl);
@@ -190,6 +194,11 @@
     }
 
     function injectStartupEvents(view) {
+        if (config.useLiveData) {
+            view.alert("Sorry, currently using live data..");
+            return;
+        }
+
         var lastStartupEvent = 32;
         while (eventNumber < lastStartupEvent) {
             injectTestEvent(view);
@@ -201,19 +210,20 @@
         bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
     }
 
-    function toggleLayout(view) {
-
-    }
-
     function cycleLabels() {
         labelIdx = (labelIdx === network.deviceLabelCount - 1) ? 0 : labelIdx + 1;
+
+        function niceLabel(label) {
+            return (label && label.trim()) ? label : '.';
+        }
+
         network.nodes.forEach(function (d) {
             var idx = (labelIdx < d.labels.length) ? labelIdx : 0,
                 node = d3.select('#' + safeId(d.id)),
                 box;
 
             node.select('text')
-                .text(d.labels[idx])
+                .text(niceLabel(d.labels[idx]))
                 .style('opacity', 0)
                 .transition()
                 .style('opacity', 1);
@@ -232,11 +242,19 @@
     }
 
     function togglePorts(view) {
-
+        view.alert('togglePorts() callback')
     }
 
     function unpin(view) {
+        view.alert('unpin() callback')
+    }
 
+    function requestPath(view) {
+        var payload = {
+            one: selections[selectOrder[0]].obj.id,
+            two: selections[selectOrder[1]].obj.id
+        }
+        sendMessage('requestPath', payload);
     }
 
     // ==============================
@@ -248,19 +266,19 @@
 //        d3.selectAll('svg .port').classed('inactive', false);
 //        d3.selectAll('svg .portText').classed('inactive', false);
         // TODO ...
-        console.log('showAllLayers()');
+        network.view.alert('showAllLayers() callback');
     }
 
     function showPacketLayer() {
         showAllLayers();
         // TODO ...
-        console.log('showPacketLayer()');
+        network.view.alert('showPacketLayer() callback');
     }
 
     function showOpticalLayer() {
         showAllLayers();
         // TODO ...
-        console.log('showOpticalLayer()');
+        network.view.alert('showOpticalLayer() callback');
     }
 
     // ==============================
@@ -279,11 +297,6 @@
         });
     }
 
-    function establishWebSocket() {
-        // TODO: establish a real web-socket
-        // NOTE, for now, we are using the 'Q' key to artificially inject
-        //       "events" from the server.
-    }
 
     // ==============================
     // Event handlers for server-pushed events
@@ -292,7 +305,8 @@
         addDevice: addDevice,
         updateDevice: updateDevice,
         removeDevice: removeDevice,
-        addLink: addLink
+        addLink: addLink,
+        showPath: showPath
     };
 
     function addDevice(data) {
@@ -331,11 +345,14 @@
         }
     }
 
+    function showPath(data) {
+        network.view.alert(data.event + "\n" + data.payload.links.length);
+    }
+
     // ....
 
     function unknownEvent(data) {
-        // TODO: use dialog, not alert
-        alert('Unknown event type: "' + data.event + '"');
+        network.view.alert('Unknown event type: "' + data.event + '"');
     }
 
     function handleServerEvent(data) {
@@ -360,7 +377,9 @@
             lnk;
 
         if (!(srcNode && dstNode)) {
-            alert('nodes not on map');
+            // TODO: send warning message back to server on websocket
+            network.view.alert('nodes not on map for link\n\n' +
+                'src = ' + src + '\ndst = ' + dst);
             return null;
         }
 
@@ -381,6 +400,7 @@
 
     function linkWidth(w) {
         // w is number of links between nodes. Scale appropriately.
+        // TODO: use a d3.scale (linear, log, ... ?)
         return w * 1.2;
     }
 
@@ -604,6 +624,7 @@
             webSock.ws.onmessage = function(m) {
                 if (m.data) {
                     console.log(m.data);
+                    handleServerEvent(JSON.parse(m.data));
                 }
             };
 
@@ -613,7 +634,7 @@
         },
 
         send : function(text) {
-            if (text != null && text.length > 0) {
+            if (text != null) {
                 webSock._send(text);
             }
         },
@@ -621,11 +642,87 @@
         _send : function(message) {
             if (webSock.ws) {
                 webSock.ws.send(message);
+            } else {
+                network.view.alert('no web socket open');
             }
         }
 
     };
 
+    var sid = 0;
+
+    function sendMessage(evType, payload) {
+        var toSend = {
+            event: evType,
+            sid: ++sid,
+            payload: payload
+        };
+        webSock.send(JSON.stringify(toSend));
+    }
+
+
+    // ==============================
+    // Selection stuff
+
+    function selectObject(obj, el) {
+        var n,
+            meta = d3.event.sourceEvent.metaKey;
+
+        if (el) {
+            n = d3.select(el);
+        } else {
+            node.each(function(d) {
+                if (d == obj) {
+                    n = d3.select(el = this);
+                }
+            });
+        }
+        if (!n) return;
+
+        if (meta && n.classed('selected')) {
+            deselectObject(obj.id);
+            //flyinPane(null);
+            return;
+        }
+
+        if (!meta) {
+            deselectAll();
+        }
+
+        selections[obj.id] = { obj: obj, el  : el};
+        selectOrder.push(obj.id);
+
+        n.classed('selected', true);
+        //flyinPane(obj);
+    }
+
+    function deselectObject(id) {
+        var obj = selections[id];
+        if (obj) {
+            d3.select(obj.el).classed('selected', false);
+            selections[id] = null;
+            // TODO: use splice to remove element
+        }
+        //flyinPane(null);
+    }
+
+    function deselectAll() {
+        // deselect all nodes in the network...
+        node.classed('selected', false);
+        selections = {};
+        selectOrder = [];
+        //flyinPane(null);
+    }
+
+
+    $('#view').on('click', function(e) {
+        if (!$(e.target).closest('.node').length) {
+            if (!e.metaKey) {
+                deselectAll();
+            }
+        }
+    });
+
     // ==============================
     // View life-cycle callbacks
 
@@ -680,7 +777,7 @@
         }
 
         function selectCb(d, self) {
-            // TODO: selectObject(d, self);
+            selectObject(d, self);
         }
 
         function atDragEnd(d, self) {
@@ -688,11 +785,22 @@
             // if it is a device (not a host)
             if (d.class === 'device') {
                 d.fixed = true;
-                d3.select(self).classed('fixed', true)
-                // TODO: send new [x,y] back to server, via websocket.
+                d3.select(self).classed('fixed', true);
+                if (config.useLiveData) {
+                    tellServerCoords(d);
+                }
             }
         }
 
+        function tellServerCoords(d) {
+            sendMessage('updateMeta', {
+                id: d.id,
+                'class': d.class,
+                x: Math.floor(d.x),
+                y: Math.floor(d.y)
+            });
+        }
+
         // set up the force layout
         network.force = d3.layout.force()
             .size(forceDim)
@@ -704,7 +812,6 @@
             .on('tick', tick);
 
         network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
-        webSock.connect();
     }
 
     function load(view, ctx) {
@@ -715,7 +822,9 @@
         view.setRadio(btnSet);
         view.setKeys(keyDispatch);
 
-        establishWebSocket();
+        if (config.useLiveData) {
+            webSock.connect();
+        }
     }
 
     function resize(view, ctx) {