Merge "Follow javadoc convention"
diff --git a/apps/foo/src/main/java/org/onlab/onos/foo/FooComponent.java b/apps/foo/src/main/java/org/onlab/onos/foo/FooComponent.java
index 4439db5..b6f1eb9 100644
--- a/apps/foo/src/main/java/org/onlab/onos/foo/FooComponent.java
+++ b/apps/foo/src/main/java/org/onlab/onos/foo/FooComponent.java
@@ -157,7 +157,9 @@
             final String someTable = "admin";
             final String someKey = "long";
 
-            dbAdminService.createTable(someTable);
+            if (!dbAdminService.listTables().contains(someTable)) {
+                dbAdminService.createTable(someTable);
+            }
 
             VersionedValue vv = dbService.get(someTable, someKey);
             if (vv == null) {
diff --git a/apps/oecfg/src/main/java/org/onlab/onos/oecfg/OELinkConfig.java b/apps/oecfg/src/main/java/org/onlab/onos/oecfg/OELinkConfig.java
index 8a1c6fc..d844f62 100644
--- a/apps/oecfg/src/main/java/org/onlab/onos/oecfg/OELinkConfig.java
+++ b/apps/oecfg/src/main/java/org/onlab/onos/oecfg/OELinkConfig.java
@@ -51,8 +51,8 @@
     private JsonNode convert(InputStream input) throws IOException {
         JsonNode json = mapper.readTree(input);
         ObjectNode result = mapper.createObjectNode();
-        result.set("opticalSwitches", opticalSwitches(json));
-        result.set("opticalLinks", opticalLinks(json));
+        result.set("switchConfig", opticalSwitches(json));
+        result.set("linkConfig", opticalLinks(json));
         return result;
     }
 
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
index cffdc09..3ae5b82 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
@@ -598,7 +598,7 @@
         Set<ConnectPoint> ingressPorts = new HashSet<>();
 
         for (Interface intf : interfaceService.getInterfaces()) {
-            if (!intf.equals(egressInterface)) {
+            if (!intf.connectPoint().equals(egressInterface.connectPoint())) {
                 ConnectPoint srcPort = intf.connectPoint();
                 ingressPorts.add(srcPort);
             }
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSessionManager.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSessionManager.java
index 8b5ed41..38fad6c 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSessionManager.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSessionManager.java
@@ -223,11 +223,6 @@
         synchronized void routeUpdates(BgpSession bgpSession,
                         Collection<BgpRouteEntry> addedBgpRouteEntries,
                         Collection<BgpRouteEntry> deletedBgpRouteEntries) {
-            //
-            // TODO: Merge the updates from different BGP Peers,
-            // by choosing the best route.
-            //
-
             // Process the deleted route entries
             for (BgpRouteEntry bgpRouteEntry : deletedBgpRouteEntries) {
                 processDeletedRoute(bgpSession, bgpRouteEntry);
diff --git a/core/api/src/main/java/org/onlab/onos/event/AbstractEvent.java b/core/api/src/main/java/org/onlab/onos/event/AbstractEvent.java
index 0fc8eaa..49fb268 100644
--- a/core/api/src/main/java/org/onlab/onos/event/AbstractEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/event/AbstractEvent.java
@@ -26,7 +26,7 @@
 
     private final long time;
     private final T type;
-    private S subject;
+    private final S subject;
 
     /**
      * Creates an event of a given type and for the specified subject and the
diff --git a/core/api/src/main/java/org/onlab/onos/store/service/ReadResult.java b/core/api/src/main/java/org/onlab/onos/store/service/ReadResult.java
index 943bc63..7aeddda 100644
--- a/core/api/src/main/java/org/onlab/onos/store/service/ReadResult.java
+++ b/core/api/src/main/java/org/onlab/onos/store/service/ReadResult.java
@@ -22,6 +22,7 @@
 
     /**
      * Returns the status of the read operation.
+     * @return read operation status
      */
     public ReadStatus status() {
         return status;
diff --git a/core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowEntryTest.java b/core/api/src/test/java/org/onlab/onos/net/flow/DefaultFlowEntryTest.java
similarity index 100%
rename from core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowEntryTest.java
rename to core/api/src/test/java/org/onlab/onos/net/flow/DefaultFlowEntryTest.java
diff --git a/core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowRuleTest.java b/core/api/src/test/java/org/onlab/onos/net/flow/DefaultFlowRuleTest.java
similarity index 100%
rename from core/net/src/test/java/org/onlab/onos/net/flow/DefaultFlowRuleTest.java
rename to core/api/src/test/java/org/onlab/onos/net/flow/DefaultFlowRuleTest.java
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
similarity index 100%
rename from core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
rename to core/api/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
diff --git a/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java b/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
index 0989867..dbb3ae4 100644
--- a/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
+++ b/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
@@ -269,7 +269,7 @@
 
         @Override
         public void notify(MastershipEvent event) {
-            log.info("dispatching mastership event {}", event);
+            log.trace("dispatching mastership event {}", event);
             eventDispatcher.post(event);
         }
 
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PathIntentInstaller.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PathIntentInstaller.java
index 9c196a2..2505be5 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PathIntentInstaller.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PathIntentInstaller.java
@@ -138,9 +138,6 @@
      * @return allocated resources if any are required, null otherwise
      */
     private LinkResourceAllocations allocateResources(PathIntent intent) {
-        if (intent.constraints() == null) {
-            return null;
-        }
         LinkResourceRequest.Builder builder =
                 DefaultLinkResourceRequest.builder(intent.id(), intent.path().links());
         for (Constraint constraint : intent.constraints()) {
diff --git a/core/net/src/test/java/org/onlab/onos/net/flow/FlowIdTest.java b/core/net/src/test/java/org/onlab/onos/net/flow/FlowIdTest.java
new file mode 100644
index 0000000..92c3bd0
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/flow/FlowIdTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.net.flow;
+
+import org.junit.Test;
+
+import com.google.common.testing.EqualsTester;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Unit tests for flow id class.
+ */
+public class FlowIdTest {
+
+    final FlowId flowId1 = FlowId.valueOf(1);
+    final FlowId sameAsFlowId1 = FlowId.valueOf(1);
+    final FlowId flowId2 = FlowId.valueOf(2);
+
+    /**
+     * Checks that the DefaultFlowRule class is immutable.
+     */
+    @Test
+    public void testImmutability() {
+        assertThatClassIsImmutable(FlowId.class);
+    }
+
+    /**
+     * Checks the operation of equals(), hashCode and toString() methods.
+     */
+    @Test
+    public void testEquals() {
+        new EqualsTester()
+                .addEqualityGroup(flowId1, sameAsFlowId1)
+                .addEqualityGroup(flowId2)
+                .testEquals();
+    }
+
+    /**
+     * Checks the construction of a FlowId object.
+     */
+    @Test
+    public void testConstruction() {
+        final long flowIdValue = 7777L;
+        final FlowId flowId = FlowId.valueOf(flowIdValue);
+        assertThat(flowId, is(notNullValue()));
+        assertThat(flowId.value(), is(flowIdValue));
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java
index 0ba67cf..97cf50e 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java
@@ -1,6 +1,5 @@
 package org.onlab.onos.store.service.impl;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static org.slf4j.LoggerFactory.getLogger;
 
 import java.util.ArrayList;
@@ -38,7 +37,6 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
 import org.onlab.onos.cluster.ClusterService;
-import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
 import org.onlab.onos.store.serializers.ImmutableListSerializer;
@@ -172,20 +170,9 @@
 
     @Override
     public ProtocolClient createClient(TcpMember member) {
-        ControllerNode remoteNode = getControllerNode(member.host(), member.port());
-        checkNotNull(remoteNode,
-                     "A valid controller node is expected for %s:%s",
-                     member.host(), member.port());
-        return new ClusterMessagingProtocolClient(
-                clusterCommunicator, clusterService.getLocalNode(), remoteNode);
-    }
-
-    private ControllerNode getControllerNode(String host, int port) {
-        for (ControllerNode node : clusterService.getNodes()) {
-            if (node.ip().toString().equals(host) && node.tcpPort() == port) {
-                return node;
-            }
-        }
-        return null;
+        return new ClusterMessagingProtocolClient(clusterService,
+                                                  clusterCommunicator,
+                                                  clusterService.getLocalNode(),
+                                                  member);
     }
 }
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 23c34b2..3dd93b9 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
@@ -13,6 +13,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
+import net.kuujo.copycat.cluster.TcpMember;
 import net.kuujo.copycat.protocol.PingRequest;
 import net.kuujo.copycat.protocol.PingResponse;
 import net.kuujo.copycat.protocol.PollRequest;
@@ -23,6 +24,9 @@
 import net.kuujo.copycat.protocol.SyncResponse;
 import net.kuujo.copycat.spi.protocol.ProtocolClient;
 
+import org.onlab.onos.cluster.ClusterEvent;
+import org.onlab.onos.cluster.ClusterEventListener;
+import org.onlab.onos.cluster.ClusterService;
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
 import org.onlab.onos.store.cluster.messaging.ClusterMessage;
@@ -43,21 +47,30 @@
 
     public static final long RETRY_INTERVAL_MILLIS = 2000;
 
+    private final ClusterService clusterService;
     private final ClusterCommunicationService clusterCommunicator;
     private final ControllerNode localNode;
-    private final ControllerNode remoteNode;
+    private final TcpMember remoteMember;
+    private ControllerNode remoteNode;
 
     // FIXME: Thread pool sizing.
     private static final ScheduledExecutorService THREAD_POOL =
             new ScheduledThreadPoolExecutor(10, THREAD_FACTORY);
 
+    private volatile CompletableFuture<Void> appeared;
+
+    private volatile InternalClusterEventListener listener;
+
     public ClusterMessagingProtocolClient(
+            ClusterService clusterService,
             ClusterCommunicationService clusterCommunicator,
             ControllerNode localNode,
-            ControllerNode remoteNode) {
+            TcpMember remoteMember) {
+
+        this.clusterService = clusterService;
         this.clusterCommunicator = clusterCommunicator;
         this.localNode = localNode;
-        this.remoteNode = remoteNode;
+        this.remoteMember = remoteMember;
     }
 
     @Override
@@ -81,15 +94,64 @@
     }
 
     @Override
-    public CompletableFuture<Void> connect() {
-        return CompletableFuture.completedFuture(null);
+    public synchronized CompletableFuture<Void> connect() {
+        if (remoteNode != null) {
+            // done
+            return CompletableFuture.completedFuture(null);
+        }
+
+        remoteNode = getControllerNode(remoteMember);
+
+        if (remoteNode != null) {
+            // done
+            return CompletableFuture.completedFuture(null);
+        }
+
+        if (appeared != null) {
+            // already waiting for member to appear
+            return appeared;
+        }
+
+        appeared = new CompletableFuture<>();
+        listener = new InternalClusterEventListener();
+        clusterService.addListener(listener);
+
+        // wait for specified controller node to come up
+        return null;
     }
 
     @Override
-    public CompletableFuture<Void> close() {
+    public synchronized CompletableFuture<Void> close() {
+        if (listener != null) {
+            clusterService.removeListener(listener);
+            listener = null;
+        }
+        if (appeared != null) {
+            appeared.cancel(true);
+            appeared = null;
+        }
         return CompletableFuture.completedFuture(null);
     }
 
+    private synchronized void checkIfMemberAppeared() {
+        final ControllerNode controllerNode = getControllerNode(remoteMember);
+        if (controllerNode == null) {
+            // still not there: no-op
+            return;
+        }
+
+        // found
+        remoteNode = controllerNode;
+        if (appeared != null) {
+            appeared.complete(null);
+        }
+
+        if (listener != null) {
+            clusterService.removeListener(listener);
+            listener = null;
+        }
+    }
+
     private <I> MessageSubject messageType(I input) {
         Class<?> clazz = input.getClass();
         if (clazz.equals(PollRequest.class)) {
@@ -112,6 +174,30 @@
         return future;
     }
 
+    private ControllerNode getControllerNode(TcpMember remoteMember) {
+        final String host = remoteMember.host();
+        final int port = remoteMember.port();
+        for (ControllerNode node : clusterService.getNodes()) {
+            if (node.ip().toString().equals(host) && node.tcpPort() == port) {
+                return node;
+            }
+        }
+        return null;
+    }
+
+    private final class InternalClusterEventListener
+            implements ClusterEventListener {
+
+        public InternalClusterEventListener() {
+        }
+
+        @Override
+        public void event(ClusterEvent event) {
+            checkIfMemberAppeared();
+        }
+
+    }
+
     private class RPCTask<I, O> implements Runnable {
 
         private final I request;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseEntryExpirationTracker.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseEntryExpirationTracker.java
new file mode 100644
index 0000000..c9775ca
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseEntryExpirationTracker.java
@@ -0,0 +1,191 @@
+/*
+ * 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.store.service.impl;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import net.jodah.expiringmap.ExpiringMap;
+import net.jodah.expiringmap.ExpiringMap.ExpirationListener;
+import net.jodah.expiringmap.ExpiringMap.ExpirationPolicy;
+import net.kuujo.copycat.cluster.Member;
+import net.kuujo.copycat.event.EventHandler;
+import net.kuujo.copycat.event.LeaderElectEvent;
+
+import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
+import org.onlab.onos.store.cluster.messaging.ClusterMessage;
+import org.onlab.onos.store.cluster.messaging.MessageSubject;
+import org.onlab.onos.store.service.DatabaseService;
+import org.onlab.onos.store.service.VersionedValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Plugs into the database update stream and track the TTL of entries added to
+ * the database. For tables with pre-configured finite TTL, this class has
+ * mechanisms for expiring (deleting) old, expired entries from the database.
+ */
+public class DatabaseEntryExpirationTracker implements
+        DatabaseUpdateEventListener, EventHandler<LeaderElectEvent> {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    public static final MessageSubject DATABASE_UPDATES = new MessageSubject(
+            "database-update-event");
+
+    private DatabaseService databaseService;
+    private ClusterService cluster;
+    private ClusterCommunicationService clusterCommunicator;
+
+    private final Member localMember;
+    private final AtomicBoolean isLocalMemberLeader = new AtomicBoolean(false);
+
+    private final Map<String, Map<DatabaseRow, VersionedValue>> tableEntryExpirationMap = new HashMap<>();
+
+    private final ExpirationListener<DatabaseRow, VersionedValue> expirationObserver = new ExpirationObserver();
+
+    DatabaseEntryExpirationTracker(Member localMember) {
+        this.localMember = localMember;
+    }
+
+    @Override
+    public void tableModified(TableModificationEvent event) {
+        DatabaseRow row = new DatabaseRow(event.tableName(), event.key());
+        Map<DatabaseRow, VersionedValue> map = tableEntryExpirationMap
+                .get(event.tableName());
+
+        switch (event.type()) {
+        case ROW_DELETED:
+            if (isLocalMemberLeader.get()) {
+                try {
+                    clusterCommunicator.broadcast(new ClusterMessage(cluster
+                            .getLocalNode().id(), DATABASE_UPDATES,
+                            DatabaseStateMachine.SERIALIZER.encode(event)));
+                } catch (IOException e) {
+                    log.error(
+                            "Failed to broadcast a database table modification event.",
+                            e);
+                }
+            }
+            break;
+        case ROW_ADDED:
+        case ROW_UPDATED:
+            map.put(row, null);
+            break;
+        default:
+            break;
+        }
+    }
+
+    @Override
+    public void tableCreated(String tableName, int expirationTimeMillis) {
+        // make this explicit instead of relying on a negative value
+        // to indicate no expiration.
+        if (expirationTimeMillis > 0) {
+            tableEntryExpirationMap.put(tableName, ExpiringMap.builder()
+                    .expiration(expirationTimeMillis, TimeUnit.SECONDS)
+                    .expirationListener(expirationObserver)
+                    // FIXME: make the expiration policy configurable.
+                    .expirationPolicy(ExpirationPolicy.CREATED).build());
+        }
+    }
+
+    @Override
+    public void tableDeleted(String tableName) {
+        tableEntryExpirationMap.remove(tableName);
+    }
+
+    private class ExpirationObserver implements
+            ExpirationListener<DatabaseRow, VersionedValue> {
+        @Override
+        public void expired(DatabaseRow key, VersionedValue value) {
+            try {
+                if (isLocalMemberLeader.get()) {
+                    if (!databaseService.removeIfVersionMatches(key.tableName,
+                            key.key, value.version())) {
+                        log.info("Entry in the database changed before right its TTL expiration.");
+                    }
+                } else {
+                    // If this node is not the current leader, we should never
+                    // let the expiring entries drop off
+                    // Under stable conditions (i.e no leadership switch) the
+                    // current leader will initiate
+                    // a database remove and this instance will get notified
+                    // of a tableModification event causing it to remove from
+                    // the map.
+                    Map<DatabaseRow, VersionedValue> map = tableEntryExpirationMap
+                            .get(key.tableName);
+                    if (map != null) {
+                        map.put(key, value);
+                    }
+                }
+
+            } catch (Exception e) {
+                log.warn(
+                        "Failed to delete entry from the database after ttl expiration. Will retry eviction",
+                        e);
+                tableEntryExpirationMap.get(key.tableName).put(
+                        new DatabaseRow(key.tableName, key.key), value);
+            }
+        }
+    }
+
+    @Override
+    public void handle(LeaderElectEvent event) {
+        if (localMember.equals(event.leader())) {
+            isLocalMemberLeader.set(true);
+        }
+    }
+
+    /**
+     * Wrapper class for a database row identifier.
+     */
+    private class DatabaseRow {
+
+        String tableName;
+        String key;
+
+        public DatabaseRow(String tableName, String key) {
+            this.tableName = tableName;
+            this.key = key;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof DatabaseRow)) {
+                return false;
+            }
+            DatabaseRow that = (DatabaseRow) obj;
+
+            return Objects.equals(this.tableName, that.tableName)
+                    && Objects.equals(this.key, that.key);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(tableName, key);
+        }
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
index fc73374..b2fe19f 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
@@ -67,6 +67,7 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected DatabaseProtocolService copycatMessagingProtocol;
 
+    // FIXME: point to appropriate path
     public static final String LOG_FILE_PREFIX = "/tmp/onos-copy-cat-log_";
 
     // Current working dir seems to be /opt/onos/apache-karaf-3.0.2
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java
index 9ca69ee..62a06b4 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java
@@ -237,8 +237,8 @@
                     WriteResult putResult = new WriteResult(WriteStatus.OK, previousValue);
                     results.add(putResult);
                     tableModificationEvent = (previousValue == null) ?
-                            TableModificationEvent.rowAdded(request.tableName(), request.key()) :
-                            TableModificationEvent.rowUpdated(request.tableName(), request.key());
+                            TableModificationEvent.rowAdded(request.tableName(), request.key(), newValue) :
+                            TableModificationEvent.rowUpdated(request.tableName(), request.key(), newValue);
                     break;
 
                 case REMOVE:
@@ -249,7 +249,7 @@
                     results.add(removeResult);
                     if (removedValue != null) {
                         tableModificationEvent =
-                                TableModificationEvent.rowDeleted(request.tableName(), request.key());
+                                TableModificationEvent.rowDeleted(request.tableName(), request.key(), removedValue);
                     }
                     break;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseUpdateEventHandler.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseUpdateEventHandler.java
deleted file mode 100644
index 21028e4..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseUpdateEventHandler.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * 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.store.service.impl;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-//import net.jodah.expiringmap.ExpiringMap;
-//import net.jodah.expiringmap.ExpiringMap.ExpirationListener;
-//import net.jodah.expiringmap.ExpiringMap.ExpirationPolicy;
-import net.kuujo.copycat.cluster.Member;
-import net.kuujo.copycat.event.EventHandler;
-import net.kuujo.copycat.event.LeaderElectEvent;
-
-import org.onlab.onos.cluster.ClusterService;
-import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
-import org.onlab.onos.store.cluster.messaging.ClusterMessage;
-import org.onlab.onos.store.cluster.messaging.MessageSubject;
-import org.onlab.onos.store.service.DatabaseService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Database update event handler.
- */
-public class DatabaseUpdateEventHandler implements
-    DatabaseUpdateEventListener, EventHandler<LeaderElectEvent> {
-
-    private final Logger log = LoggerFactory.getLogger(getClass());
-
-    public static final MessageSubject DATABASE_UPDATES =
-            new MessageSubject("database-update-event");
-
-    private DatabaseService databaseService;
-    private ClusterService cluster;
-    private ClusterCommunicationService clusterCommunicator;
-
-    private final Member localMember;
-    private final AtomicBoolean isLocalMemberLeader = new AtomicBoolean(false);
-    private final Map<String, Map<DatabaseRow, Void>> tableEntryExpirationMap = new HashMap<>();
-    //private final ExpirationListener<DatabaseRow, Void> expirationObserver = new ExpirationObserver();
-
-    DatabaseUpdateEventHandler(Member localMember) {
-        this.localMember = localMember;
-    }
-
-    @Override
-    public void tableModified(TableModificationEvent event) {
-        DatabaseRow row = new DatabaseRow(event.tableName(), event.key());
-        Map<DatabaseRow, Void> map = tableEntryExpirationMap.get(event.tableName());
-
-        switch (event.type()) {
-        case ROW_DELETED:
-            if (isLocalMemberLeader.get()) {
-                try {
-                    clusterCommunicator.broadcast(
-                            new ClusterMessage(
-                                    cluster.getLocalNode().id(),
-                                    DATABASE_UPDATES,
-                                    DatabaseStateMachine.SERIALIZER.encode(event)));
-                } catch (IOException e) {
-                    log.error("Failed to broadcast a database table modification event.", e);
-                }
-            }
-            break;
-        case ROW_ADDED:
-        case ROW_UPDATED:
-            map.put(row, null);
-            break;
-        default:
-            break;
-        }
-    }
-
-    @Override
-    public void tableCreated(String tableName, int expirationTimeMillis) {
-        // make this explicit instead of relying on a negative value
-        // to indicate no expiration.
-        if (expirationTimeMillis > 0) {
-            tableEntryExpirationMap.put(tableName, null);
-            /*
-            ExpiringMap.builder()
-                    .expiration(expirationTimeMillis, TimeUnit.SECONDS)
-                    .expirationListener(expirationObserver)
-                    // FIXME: make the expiration policy configurable.
-                    .expirationPolicy(ExpirationPolicy.CREATED)
-                    .build());
-                    */
-        }
-    }
-
-    @Override
-    public void tableDeleted(String tableName) {
-        tableEntryExpirationMap.remove(tableName);
-    }
-
-    /*
-    private class ExpirationObserver implements ExpirationListener<DatabaseRow, Void> {
-        @Override
-        public void expired(DatabaseRow key, Void value) {
-            try {
-                // TODO: The safety of this check needs to be verified.
-                // Couple of issues:
-                // 1. It is very likely that only one member should attempt deletion of the entry from database.
-                // 2. A potential race condition exists where the entry expires, but before its can be deleted
-                // from the database, a new entry is added or existing entry is updated.
-                // That means ttl and expiration should be for a given version.
-                if (isLocalMemberLeader.get()) {
-                    databaseService.remove(key.tableName, key.key);
-                }
-            } catch (Exception e) {
-                log.warn("Failed to delete entry from the database after ttl expiration. Will retry eviction", e);
-                tableEntryExpirationMap.get(key.tableName).put(new DatabaseRow(key.tableName, key.key), null);
-            }
-        }
-    }
-    */
-
-    @Override
-    public void handle(LeaderElectEvent event) {
-        if (localMember.equals(event.leader())) {
-            isLocalMemberLeader.set(true);
-        }
-    }
-
-    private class DatabaseRow {
-
-        String tableName;
-        String key;
-
-        public DatabaseRow(String tableName, String key) {
-            this.tableName = tableName;
-            this.key = key;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) {
-                return true;
-            }
-            if (!(obj instanceof DatabaseRow)) {
-                return false;
-            }
-            DatabaseRow that = (DatabaseRow) obj;
-
-            return Objects.equals(this.tableName, that.tableName) &&
-                   Objects.equals(this.key, that.key);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(tableName, key);
-        }
-    }
-}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseUpdateEventListener.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseUpdateEventListener.java
index d97191c..1dc0e9d 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseUpdateEventListener.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseUpdateEventListener.java
@@ -29,15 +29,14 @@
 
     /**
      * Notifies listeners of a table created event.
-     * @param tableName
-     * @param expirationTimeMillis
+     * @param tableName name of the table created
+     * @param expirationTimeMillis TTL for entries added to the table (measured since last update time)
      */
     public void tableCreated(String tableName, int expirationTimeMillis);
 
     /**
      * Notifies listeners of a table deleted event.
-     * @param tableName
+     * @param tableName name of the table deleted
      */
     public void tableDeleted(String tableName);
-
 }
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DistributedLockManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DistributedLockManager.java
index f83b042..6d99ba7 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DistributedLockManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DistributedLockManager.java
@@ -33,7 +33,8 @@
 
     public static final String ONOS_LOCK_TABLE_NAME = "onos-locks";
 
-    private final ArrayListMultimap<String, LockRequest> locksToAcquire = ArrayListMultimap.create();
+    private final ArrayListMultimap<String, LockRequest> locksToAcquire = ArrayListMultimap
+            .create();
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     private ClusterCommunicationService clusterCommunicator;
@@ -61,11 +62,7 @@
 
     @Override
     public Lock create(String path) {
-        return new DistributedLock(
-                path,
-                databaseService,
-                clusterService,
-                this);
+        return new DistributedLock(path, databaseService, clusterService, this);
     }
 
     @Override
@@ -80,21 +77,19 @@
         throw new UnsupportedOperationException();
     }
 
-    protected CompletableFuture<Void> lockIfAvailable(
-            Lock lock,
-            long waitTimeMillis,
-            int leaseDurationMillis) {
+    protected CompletableFuture<Void> lockIfAvailable(Lock lock,
+            long waitTimeMillis, int leaseDurationMillis) {
         CompletableFuture<Void> future = new CompletableFuture<>();
-        locksToAcquire.put(
-                lock.path(),
-                new LockRequest(lock, waitTimeMillis, leaseDurationMillis, future));
+        locksToAcquire.put(lock.path(), new LockRequest(lock, waitTimeMillis,
+                leaseDurationMillis, future));
         return future;
     }
 
     private class LockEventMessageListener implements ClusterMessageHandler {
         @Override
         public void handle(ClusterMessage message) {
-            TableModificationEvent event = DatabaseStateMachine.SERIALIZER.decode(message.payload());
+            TableModificationEvent event = DatabaseStateMachine.SERIALIZER
+                    .decode(message.payload());
             if (!event.tableName().equals(ONOS_LOCK_TABLE_NAME)) {
                 return;
             }
@@ -110,15 +105,20 @@
                     return;
                 }
 
-                Iterator<LockRequest> existingRequestIterator = existingRequests.iterator();
-                while (existingRequestIterator.hasNext()) {
-                    LockRequest request = existingRequestIterator.next();
-                    if (request.expirationTime().isAfter(DateTime.now())) {
-                        existingRequestIterator.remove();
-                    } else {
-                        if (request.lock().tryLock(request.leaseDurationMillis())) {
-                            request.future().complete(null);
-                            existingRequests.remove(0);
+                synchronized (existingRequests) {
+
+                    Iterator<LockRequest> existingRequestIterator = existingRequests
+                            .iterator();
+                    while (existingRequestIterator.hasNext()) {
+                        LockRequest request = existingRequestIterator.next();
+                        if (request.expirationTime().isAfter(DateTime.now())) {
+                            existingRequestIterator.remove();
+                        } else {
+                            if (request.lock().tryLock(
+                                    request.leaseDurationMillis())) {
+                                request.future().complete(null);
+                                existingRequestIterator.remove();
+                            }
                         }
                     }
                 }
@@ -133,14 +133,12 @@
         private final int leaseDurationMillis;
         private final CompletableFuture<Void> future;
 
-        public LockRequest(
-            Lock lock,
-            long waitTimeMillis,
-            int leaseDurationMillis,
-            CompletableFuture<Void> future) {
+        public LockRequest(Lock lock, long waitTimeMillis,
+                int leaseDurationMillis, CompletableFuture<Void> future) {
 
             this.lock = lock;
-            this.expirationTime = DateTime.now().plusMillis((int) waitTimeMillis);
+            this.expirationTime = DateTime.now().plusMillis(
+                    (int) waitTimeMillis);
             this.leaseDurationMillis = leaseDurationMillis;
             this.future = future;
         }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TableModificationEvent.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TableModificationEvent.java
index 885c9fa..b0962dc 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TableModificationEvent.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TableModificationEvent.java
@@ -1,5 +1,7 @@
 package org.onlab.onos.store.service.impl;
 
+import org.onlab.onos.store.service.VersionedValue;
+
 /**
  * A table modification event.
  */
@@ -17,41 +19,46 @@
 
     private final String tableName;
     private final String key;
+    private final VersionedValue value;
     private final Type type;
 
     /**
      * Creates a new row deleted table modification event.
      * @param tableName table name.
      * @param key row key
+     * @param value value associated with the key when it was deleted.
      * @return table modification event.
      */
-    public static TableModificationEvent rowDeleted(String tableName, String key) {
-        return new TableModificationEvent(tableName, key, Type.ROW_DELETED);
+    public static TableModificationEvent rowDeleted(String tableName, String key, VersionedValue value) {
+        return new TableModificationEvent(tableName, key, value, Type.ROW_DELETED);
     }
 
     /**
      * Creates a new row added table modification event.
      * @param tableName table name.
      * @param key row key
+     * @param value value associated with the key
      * @return table modification event.
      */
-    public static TableModificationEvent rowAdded(String tableName, String key) {
-        return new TableModificationEvent(tableName, key, Type.ROW_ADDED);
+    public static TableModificationEvent rowAdded(String tableName, String key, VersionedValue value) {
+        return new TableModificationEvent(tableName, key, value, Type.ROW_ADDED);
     }
 
     /**
      * Creates a new row updated table modification event.
      * @param tableName table name.
      * @param key row key
+     * @param newValue value
      * @return table modification event.
      */
-    public static TableModificationEvent rowUpdated(String tableName, String key) {
-        return new TableModificationEvent(tableName, key, Type.ROW_UPDATED);
+    public static TableModificationEvent rowUpdated(String tableName, String key, VersionedValue newValue) {
+        return new TableModificationEvent(tableName, key, newValue, Type.ROW_UPDATED);
     }
 
-    private TableModificationEvent(String tableName, String key, Type type) {
+    private TableModificationEvent(String tableName, String key, VersionedValue value, Type type) {
         this.tableName = tableName;
         this.key = key;
+        this.value = value;
         this.type = type;
     }
 
@@ -72,6 +79,15 @@
     }
 
     /**
+     * Returns the value associated with the key. If the event for a deletion, this
+     * method returns value that was deleted.
+     * @return row value
+     */
+    public VersionedValue value() {
+        return value;
+    }
+
+    /**
      * Returns the type of table modification event.
      * @return event type.
      */
diff --git a/tools/package/etc/hazelcast.xml b/tools/package/etc/hazelcast.xml
index b950768..b92a793 100644
--- a/tools/package/etc/hazelcast.xml
+++ b/tools/package/etc/hazelcast.xml
@@ -176,7 +176,7 @@
             com.hazelcast.map.merge.HigherHitsMapMergePolicy ; entry with the higher hits wins.
             com.hazelcast.map.merge.LatestUpdateMapMergePolicy ; entry with the latest update wins.
         -->
-        <merge-policy>com.hazelcast.map.merge.PassThroughMergePolicy</merge-policy>
+        <merge-policy>com.hazelcast.map.merge.PutIfAbsentMapMergePolicy</merge-policy>
 
     </map>
 
diff --git a/tools/test/bin/onos-push-update-bundle b/tools/test/bin/onos-push-update-bundle
index 1539467..f8d682a 100755
--- a/tools/test/bin/onos-push-update-bundle
+++ b/tools/test/bin/onos-push-update-bundle
@@ -13,6 +13,8 @@
 
 bundle=$(echo $(basename $jar .jar) | sed 's/-[0-9].*//g')
 
+echo "pushing bundle: $bundle"
+
 nodes=$(env | sort | egrep "OC[0-9]+" | cut -d= -f2)
 for node in $nodes; do
     scp -q $jar $ONOS_USER@$node:.m2/repository/$jar
diff --git a/tools/test/topos/oe-nonlinear-10.json b/tools/test/topos/oe-nonlinear-10.json
index 522215b..59c5b89 100644
--- a/tools/test/topos/oe-nonlinear-10.json
+++ b/tools/test/topos/oe-nonlinear-10.json
@@ -61,14 +61,6 @@
             "ports": [ { "port": 30, "speed": 0, "type": "FIBER" }, { "port": 31, "speed": 0, "type": "FIBER" } ]
         },
         {
-            "uri": "of:0000ffffffffff06", "mac": "ffffffffffff06", "type": "ROADM",
-            "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?", "name": "DFW-M10",
-            "annotations": { "latitude": 32.8, "longitude": 97.1, "optical.regens": 3 },
-            "ports": [ { "port": 30, "speed": 0, "type": "FIBER" }, { "port": 31, "speed": 0, "type": "FIBER" } ]
-        },
-
-
-        {
             "uri": "of:0000ffffffff0001", "mac": "ffffffffff0001", "type": "SWITCH",
             "mfr": "Linc", "hw": "PK", "sw": "?", "serial": "?", "name": "SFO-R10",
             "annotations": { "latitude": 37.6, "longitude": 122.3 },
diff --git a/tools/test/topos/oe-nonlinear-4.json b/tools/test/topos/oe-nonlinear-4.json
new file mode 100644
index 0000000..ac86a01
--- /dev/null
+++ b/tools/test/topos/oe-nonlinear-4.json
@@ -0,0 +1,51 @@
+{
+    "devices" : [
+        {
+            "uri": "of:0000ffffffffff01", "mac": "ffffffffffff01", "type": "ROADM",
+            "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?", "name": "ROADM1",
+            "annotations": { "latitude": 37.6, "longitude": 122.3, "optical.regens": 0 },
+            "ports": [ { "port": 10, "speed": 100000, "type": "FIBER" }, { "port": 20, "speed": 0, "type": "FIBER" }, { "port": 22, "speed": 0, "type": "FIBER" }]
+        },
+        {
+            "uri": "of:0000ffffffffff02", "mac": "ffffffffffff02", "type": "ROADM",
+            "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?", "name": "ROADM2",
+            "annotations": { "latitude": 37.3, "longitude": 121.9, "optical.regens": 0 },
+            "ports": [ { "port": 11, "speed": 100000, "type": "FIBER" }, { "port": 21, "speed": 0, "type": "FIBER" }, { "port": 22, "speed": 0, "type": "FIBER" }]
+        },
+        {
+            "uri": "of:0000ffffffffff03", "mac": "ffffffffffff03", "type": "ROADM",
+            "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?", "name": "ROADM3",
+            "annotations": { "latitude": 33.9, "longitude": 118.4, "optical.regens": 2 },
+            "ports": [ { "port": 30, "speed": 0, "type": "FIBER" }, { "port": 31, "speed": 0, "type": "FIBER" }]
+        },
+	{
+            "uri": "of:0000ffffffffff04", "mac": "ffffffffffff04", "type":"ROADM",
+            "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?", "name": "ROADM4",
+            "annotations": { "latitude": 39.9, "longitude": 119.4, "optical.regens": 2 },
+            "ports": [ { "port": 30, "speed": 0, "type": "FIBER" }, { "port": 31, "speed": 0, "type": "FIBER" }]
+        },
+        {
+            "uri": "of:0000ffffffff0001", "mac": "ffffffffff0001", "type": "SWITCH",
+            "mfr": "Linc", "hw": "PK", "sw": "?", "serial": "?", "name": "ROUTER1",
+            "annotations": { "latitude": 37.6, "longitude": 122.3 },
+            "ports": [ { "port": 1, "speed": 10000, "type": "COPPER" }, { "port": 2, "speed": 100000, "type": "FIBER" } ]
+        },
+        {
+            "uri": "of:0000ffffffff0002", "mac": "ffffffffff0002", "type": "SWITCH",
+            "mfr": "Linc", "hw": "PK", "sw": "?", "serial": "?", "name": "ROUTER2",
+            "annotations": { "latitude": 37.3, "longitude": 121.9 },
+            "ports": [ { "port": 1, "speed": 10000, "type": "COPPER" }, { "port": 2, "speed": 100000, "type": "FIBER" } ]
+        }
+    ],
+
+    "links" : [
+        { "src": "of:0000ffffffffff01/20", "dst": "of:0000ffffffffff03/30", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "durable": "true" } },
+        { "src": "of:0000ffffffffff02/21", "dst": "of:0000ffffffffff03/31", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "durable": "true" } },
+        { "src": "of:0000ffffffffff01/22", "dst": "of:0000ffffffffff04/30", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "durable": "true" } },
+        { "src": "of:0000ffffffffff04/31", "dst": "of:0000ffffffffff02/22", "type": "OPTICAL", "annotations": { "optical.waves": 80, "optical.type": "WDM", "durable": "true" } },
+
+        { "src": "of:0000ffffffff0001/2", "dst": "of:0000ffffffffff01/10", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } },
+        { "src": "of:0000ffffffff0002/2", "dst": "of:0000ffffffffff02/11", "type": "OPTICAL", "annotations": { "bandwidth": 100000, "optical.type": "cross-connect", "durable": "true" } }
+    ]
+
+}
diff --git a/utils/thirdparty/pom.xml b/utils/thirdparty/pom.xml
index 164f7c8..58c6a9b 100644
--- a/utils/thirdparty/pom.xml
+++ b/utils/thirdparty/pom.xml
@@ -39,30 +39,23 @@
     </dependency>
 
         <dependency>
+          <groupId>net.jodah</groupId>
+          <artifactId>expiringmap</artifactId>
+          <version>0.3.1</version>
+        </dependency>
+
+        <dependency>
             <groupId>net.kuujo.copycat</groupId>
             <artifactId>copycat</artifactId>
             <version>${copycat.version}</version>
         </dependency>
-<!-- Commented out due to Chronicle + OSGi issue 
-        <dependency>
-            <groupId>net.kuujo.copycat</groupId>
-            <artifactId>copycat-chronicle</artifactId>
-            <version>${copycat.version}</version>
-        </dependency>
--->
+
         <dependency>
             <groupId>net.kuujo.copycat</groupId>
             <artifactId>copycat-tcp</artifactId>
             <version>${copycat.version}</version>
         </dependency>
 
-<!-- chronicle transitive dependency
-        <dependency>
-            <groupId>net.java.dev.jna</groupId>
-            <artifactId>jna</artifactId>
-            <version>4.1.0</version>
-        </dependency>
--->
   </dependencies>
 
   <build>
@@ -89,20 +82,19 @@
             </filter>
 
             <filter>
+              <artifact>net.jodah.expiringmap:*</artifact>
+              <includes>
+                <include>net/jodah/expiringmap/**</include>
+              </includes>
+            </filter>
+
+            <filter>
               <artifact>net.kuujo.copycat:*</artifact>
               <includes>
                 <include>net/kuujo/copycat/**</include>
               </includes>
             </filter>
-<!-- chronicle transitive dependency
 
-            <filter>
-              <artifact>net.java.dev.jna:*</artifact>
-              <includes>
-                <include>com/sun/jna/**</include>
-              </includes>
-            </filter>
--->
           </filters>
         </configuration>
         <executions>
@@ -120,7 +112,7 @@
         <configuration>
           <instructions>
             <Export-Package>
-              com.googlecode.concurrenttrees.*;net.kuujo.copycat.*
+              com.googlecode.concurrenttrees.*;net.kuujo.copycat.*;net.jodah.expiringmap.*
             </Export-Package>
           </instructions>
         </configuration>
diff --git a/web/gui/src/main/webapp/json/ev/_capture/rx/showPath_ex2.json b/web/gui/src/main/webapp/json/ev/_capture/rx/showPath_ex2.json
new file mode 100644
index 0000000..2a05249
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/rx/showPath_ex2.json
@@ -0,0 +1,11 @@
+{
+  "event": "showPath",
+  "sid": 3,
+  "payload": {
+    "ids": [
+      "of:0000000000000007"
+    ],
+    "traffic": true
+  }
+}
+// what is the client supposed to do with this?
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex1_devs.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex1_devs.json
new file mode 100644
index 0000000..725c15f
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex1_devs.json
@@ -0,0 +1,11 @@
+{
+  "event": "requestTraffic",
+  "sid": 6,
+  "payload": {
+    "ids": [
+      "of:0000000000000007",
+      "of:000000000000000c",
+      "of:000000000000000a"
+    ]
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex2_hosts.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex2_hosts.json
new file mode 100644
index 0000000..84f17df
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex2_hosts.json
@@ -0,0 +1,12 @@
+{
+  "event": "requestTraffic",
+  "sid": 12,
+  "payload": {
+    "ids": [
+      "86:C3:7B:90:79:CD/-1",
+      "22:BA:28:81:FD:45/-1",
+      "BA:91:F6:8E:B6:B6/-1",
+      "06:E2:E6:F7:03:12/-1"
+    ]
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex3_devs_hosts.json b/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex3_devs_hosts.json
new file mode 100644
index 0000000..3f915df
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/_capture/tx/requestTraffic_ex3_devs_hosts.json
@@ -0,0 +1,12 @@
+{
+  "event": "requestTraffic",
+  "sid": 18,
+  "payload": {
+    "ids": [
+      "of:0000000000000001",
+      "86:C3:7B:90:79:CD/-1",
+      "7E:D2:EE:0F:12:4A/-1",
+      "of:000000000000000c"
+    ]
+  }
+}
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 0644c32..7632148 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -115,7 +115,6 @@
         }
 
         function doError(msg) {
-            errorCount++;
             console.error(msg);
             doAlert(msg);
         }
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index aeaad2d..a8c67a1 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -24,14 +24,33 @@
     opacity: 0.5;
 }
 
+
 /* NODES */
 
-#topo svg .node.device {
-    stroke: none;
-    stroke-width: 1.5px;
+#topo svg .node {
     cursor: pointer;
 }
 
+#topo svg .node.selected rect,
+#topo svg .node.selected circle {
+    filter: url(#blue-glow);
+}
+
+/* for debugging */
+#topo svg .node circle.debug {
+    fill: white;
+    stroke: red;
+}
+
+#topo svg .node text {
+    pointer-events: none;
+}
+
+/* Device Nodes */
+
+#topo svg .node.device {
+}
+
 #topo svg .node.device rect {
     stroke-width: 1.5px;
 }
@@ -54,31 +73,28 @@
     fill: #03c;
 }
 
-#topo svg .node.host {
-    fill: #846;
-}
-
 /* note: device is offline without the 'online' class */
 #topo svg .node.device text {
     fill: #aaa;
     font: 10pt sans-serif;
-    pointer-events: none;
 }
 
 #topo svg .node.device.online text {
     fill: white;
 }
 
+
+/* Host Nodes */
+
+#topo svg .node.host {
+    fill: #846;
+}
+
 #topo svg .node.host text {
     fill: #846;
     font: 9pt sans-serif;
-    pointer-events: none;
 }
 
-#topo svg .node.selected rect,
-#topo svg .node.selected circle {
-    filter: url(#blue-glow);
-}
 
 /* LINKS */
 
@@ -91,20 +107,13 @@
     stroke-width: 6px;
 }
 
-/* for debugging */
-#topo svg .node circle.debug {
-    fill: white;
-    stroke: red;
-}
 
-
-/* detail topo-detail pane */
+/* Fly-in details pane */
 
 #topo-detail {
 /* gets base CSS from .fpanel in floatPanel.css */
 }
 
-
 #topo-detail h2 {
     margin: 8px 4px;
     color: black;
@@ -128,9 +137,10 @@
 }
 
 #topo-detail td.value {
-
 }
 
+
+
 #topo-detail hr {
     height: 1px;
     color: #ccc;
@@ -138,3 +148,24 @@
     border: 0;
 }
 
+/* Web Socket Closed Mask (starts hidden) */
+
+#topo-mask {
+    display: none;
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 10000px;
+    height: 8000px;
+    z-index: 5000;
+    background-color: rgba(0,0,0,0.75);
+    padding: 60px;
+}
+
+#topo-mask p {
+    margin: 8px 20px;
+    color: #ddd;
+    font-size: 14pt;
+    font-style: italic;
+}
+
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index a23f48d..8a3bc5d 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -127,7 +127,9 @@
         P: togglePorts,
         U: unpin,
 
-        X: requestPath
+        W: requestTraffic,  // bag of selections
+        Z: requestPath,     // host-to-host intent (and monitor)
+        X: cancelMonitor
     };
 
     // state variables
@@ -150,6 +152,7 @@
             debug: false
         },
         webSock,
+        sid = 0,
         deviceLabelIndex = 0,
         hostLabelIndex = 0,
         detailPane,
@@ -168,7 +171,8 @@
         nodeG,
         linkG,
         node,
-        link;
+        link,
+        mask;
 
     // ==============================
     // For Debugging / Development
@@ -192,15 +196,11 @@
 
     function testMe(view) {
         view.alert('test');
-        detailPane.show();
-        setTimeout(function () {
-            detailPane.hide();
-        }, 3000);
     }
 
     function abortIfLive() {
         if (config.useLiveData) {
-            scenario.view.alert("Sorry, currently using live data..");
+            network.view.alert("Sorry, currently using live data..");
             return true;
         }
         return false;
@@ -343,14 +343,18 @@
         addDevice: addDevice,
         addLink: addLink,
         addHost: addHost,
+
         updateDevice: updateDevice,
         updateLink: updateLink,
         updateHost: updateHost,
+
         removeDevice: stillToImplement,
         removeLink: removeLink,
         removeHost: removeHost,
+
         showDetails: showDetails,
-        showPath: showPath
+        showPath: showPath,
+        showTraffic: showTraffic
     };
 
     function addDevice(data) {
@@ -463,6 +467,7 @@
     function showDetails(data) {
         fnTrace('showDetails', data.payload.id);
         populateDetails(data.payload);
+        // TODO: Add single-select actions ...
         detailPane.show();
     }
 
@@ -485,6 +490,10 @@
         // TODO: add selection-highlite lines to links
     }
 
+    function showTraffic(data) {
+        network.view.alert("showTraffic() -- TODO")
+    }
+
     // ...............................
 
     function stillToImplement(data) {
@@ -505,17 +514,58 @@
     // ==============================
     // Out-going messages...
 
+    function userFeedback(msg) {
+        // for now, use the alert pane as is. Maybe different alert style in
+        // the future (centered on view; dismiss button?)
+        network.view.alert(msg);
+    }
+
+    function nSel() {
+        return selectOrder.length;
+    }
     function getSel(idx) {
         return selections[selectOrder[idx]];
     }
+    function getSelId(idx) {
+        return getSel(idx).obj.id;
+    }
+    function allSelectionsClass(cls) {
+        for (var i=0, n=nSel(); i<n; i++) {
+            if (getSel(i).obj.class !== cls) {
+                return false;
+            }
+        }
+        return true;
+    }
 
-    // for now, just a host-to-host intent, (and implicit start-monitoring)
+    function requestTraffic() {
+        if (nSel() > 0) {
+            sendMessage('requestTraffic', {
+                ids: selectOrder
+            });
+        } else {
+            userFeedback('Request-Traffic requires one or\n' +
+                         'more items to be selected.');
+        }
+    }
+
     function requestPath() {
-        var payload = {
-                one: getSel(0).obj.id,
-                two: getSel(1).obj.id
-            };
-        sendMessage('requestPath', payload);
+        if (nSel() === 2 && allSelectionsClass('host')) {
+            sendMessage('requestPath', {
+                one: getSelId(0),
+                two: getSelId(1)
+            });
+        } else {
+            userFeedback('Request-Path requires two\n' +
+                'hosts to be selected.');
+        }
+    }
+
+    function cancelMonitor() {
+        // FIXME: from where do we get the intent id(s) to send to the server?
+        sendMessage('cancelMonitor', {
+            ids: ["need_the_intent_id"]
+        });
     }
 
     // request details for the selected element
@@ -701,18 +751,57 @@
 
     function positionNode(node) {
         var meta = node.metaUi,
-            x = 0,
-            y = 0;
+            x = meta && meta.x,
+            y = meta && meta.y,
+            xy;
 
-        if (meta) {
-            x = meta.x;
-            y = meta.y;
-        }
+        // If we have [x,y] already, use that...
         if (x && y) {
             node.fixed = true;
+            node.x = x;
+            node.y = y;
+            return;
         }
-        node.x = x || network.view.width() / 2;
-        node.y = y || network.view.height() / 2;
+
+        // Note: Placing incoming unpinned nodes at exactly the same point
+        //        (center of the view) causes them to explode outwards when
+        //        the force layout kicks in. So, we spread them out a bit
+        //        initially, to provide a more serene layout convergence.
+        //       Additionally, if the node is a host, we place it near
+        //        the device it is connected to.
+
+        function spread(s) {
+            return Math.floor((Math.random() * s) - s/2);
+        }
+
+        function randDim(dim) {
+            return dim / 2 + spread(dim * 0.7071);
+        }
+
+        function rand() {
+            return {
+                x: randDim(network.view.width()),
+                y: randDim(network.view.height())
+            };
+        }
+
+        function near(node) {
+            var min = 12,
+                dx = spread(12),
+                dy = spread(12);
+            return {
+                x: node.x + min + dx,
+                y: node.y + min + dy
+            };
+        }
+
+        function getDevice(cp) {
+            var d = network.lookup[cp.device];
+            return d || rand();
+        }
+
+        xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+        $.extend(node, xy);
     }
 
     function iconUrl(d) {
@@ -1012,6 +1101,7 @@
             webSock.ws = new WebSocket(webSockUrl());
 
             webSock.ws.onopen = function() {
+                noWebSock(false);
             };
 
             webSock.ws.onmessage = function(m) {
@@ -1023,6 +1113,7 @@
 
             webSock.ws.onclose = function(m) {
                 webSock.ws = null;
+                noWebSock(true);
             };
         },
 
@@ -1042,7 +1133,9 @@
 
     };
 
-    var sid = 0;
+    function noWebSock(b) {
+        mask.style('display',b ? 'block' : 'none');
+    }
 
     // TODO: use cache of pending messages (key = sid) to reconcile responses
 
@@ -1100,7 +1193,7 @@
             deselectAll();
         }
 
-        selections[obj.id] = { obj: obj, el  : el};
+        selections[obj.id] = { obj: obj, el: el };
         selectOrder.push(obj.id);
 
         n.classed('selected', true);
@@ -1108,12 +1201,16 @@
     }
 
     function deselectObject(id) {
-        var obj = selections[id];
+        var obj = selections[id],
+            idx;
         if (obj) {
             d3.select(obj.el).classed('selected', false);
             delete selections[id];
+            idx = $.inArray(id, selectOrder);
+            if (idx >= 0) {
+                selectOrder.splice(idx, 1);
+            }
         }
-        updateDetailPane();
     }
 
     function deselectAll() {
@@ -1147,12 +1244,43 @@
 
     function singleSelect() {
         requestDetails();
-        // NOTE: detail pane will be shown from showDetails event.
+        // NOTE: detail pane will be shown from showDetails event callback
     }
 
     function multiSelect() {
-        // TODO: use detail pane for multi-select view.
-        //detailPane.show();
+        populateMultiSelect();
+        // TODO: Add multi-select actions ...
+    }
+
+    function addSep(tbody) {
+        var tr = tbody.append('tr');
+        $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
+    }
+
+    function addProp(tbody, label, value) {
+        var tr = tbody.append('tr');
+
+        tr.append('td')
+            .attr('class', 'label')
+            .text(label + ' :');
+
+        tr.append('td')
+            .attr('class', 'value')
+            .text(value);
+    }
+
+    function populateMultiSelect() {
+        detailPane.empty();
+
+        var title = detailPane.append("h2"),
+            table = detailPane.append("table"),
+            tbody = table.append("tbody");
+
+        title.text('Multi-Select...');
+
+        selectOrder.forEach(function (d, i) {
+            addProp(tbody, i+1, d);
+        });
     }
 
     function populateDetails(data) {
@@ -1172,23 +1300,6 @@
                 addProp(tbody, p, data.props[p]);
             }
         });
-
-        function addSep(tbody) {
-            var tr = tbody.append('tr');
-            $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
-        }
-
-        function addProp(tbody, label, value) {
-            var tr = tbody.append('tr');
-
-            tr.append('td')
-                .attr('class', 'label')
-                .text(label + ' :');
-
-            tr.append('td')
-                .attr('class', 'value')
-                .text(value);
-        }
     }
 
     // ==============================
@@ -1226,6 +1337,11 @@
 
     }
 
+
+    function para(sel, text) {
+        sel.append('p').text(text);
+    }
+
     // ==============================
     // View life-cycle callbacks
 
@@ -1320,6 +1436,12 @@
             .on('tick', tick);
 
         network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
+
+        // create mask layer for when we lose connection to server.
+        mask = view.$div.append('div').attr('id','topo-mask');
+        para(mask, 'Oops!');
+        para(mask, 'Web-socket connection to server closed...');
+        para(mask, 'Try refreshing the page.');
     }
 
     function load(view, ctx, flags) {