Merge "Remove null check because of guarantee of non-null"
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/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/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 dc913bf..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,
-                     "No matching ONOS Node 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/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 0dfa466..a8c67a1 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -139,6 +139,8 @@
 #topo-detail td.value {
 }
 
+
+
 #topo-detail hr {
     height: 1px;
     color: #ccc;
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 48e4851..8a3bc5d 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -127,7 +127,8 @@
         P: togglePorts,
         U: unpin,
 
-        Z: requestPath,
+        W: requestTraffic,  // bag of selections
+        Z: requestPath,     // host-to-host intent (and monitor)
         X: cancelMonitor
     };
 
@@ -199,7 +200,7 @@
 
     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;
@@ -342,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) {
@@ -462,6 +467,7 @@
     function showDetails(data) {
         fnTrace('showDetails', data.payload.id);
         populateDetails(data.payload);
+        // TODO: Add single-select actions ...
         detailPane.show();
     }
 
@@ -484,6 +490,10 @@
         // TODO: add selection-highlite lines to links
     }
 
+    function showTraffic(data) {
+        network.view.alert("showTraffic() -- TODO")
+    }
+
     // ...............................
 
     function stillToImplement(data) {
@@ -504,24 +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() {
-        var payload = {
-                id: "need_the_intent_id"  // FIXME: where are we storing this?
-            };
-        sendMessage('cancelMonitor', payload);
+        // 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
@@ -1200,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) {
@@ -1225,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);
-        }
     }
 
     // ==============================