Merge "Changed LOCAL port number in the CLI to "local"."
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 38e1322..849ad17 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
@@ -159,7 +159,7 @@
             return messagingService.sendAndReceive(nodeEp, message.subject().value(), SERIALIZER.encode(message));
 
         } catch (IOException e) {
-            log.error("Failed interaction with remote nodeId: " + toNodeId, e);
+            log.trace("Failed interaction with remote nodeId: " + toNodeId, e);
             throw e;
         }
     }
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 c561221..56dba79 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
@@ -149,12 +149,12 @@
 
     @Activate
     public void activate() {
-        log.info("Started.");
+        log.info("Started");
     }
 
     @Deactivate
     public void deactivate() {
-        log.info("Stopped.");
+        log.info("Stopped");
     }
 
     @Override
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 19ee882..2779b35 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
@@ -5,6 +5,8 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 import net.kuujo.copycat.Copycat;
 import net.kuujo.copycat.StateMachine;
@@ -60,9 +62,11 @@
     private Copycat copycat;
     private DatabaseClient client;
 
-    // TODO: check if synchronization is required to read/modify this
+    // guarded by synchronized block
     private ClusterConfig<TcpMember> clusterConfig;
 
+    private CountDownLatch clusterEventLatch;
+
     private ClusterEventListener clusterEventListener;
 
     @Activate
@@ -81,22 +85,45 @@
 
         List<TcpMember> remoteMembers = new ArrayList<>(clusterService.getNodes().size());
 
+        clusterEventLatch = new CountDownLatch(1);
         clusterEventListener = new InternalClusterEventListener();
         clusterService.addListener(clusterEventListener);
 
+        // note: from this point beyond, clusterConfig requires synchronization
+
         for (ControllerNode node : clusterService.getNodes()) {
             TcpMember member = new TcpMember(node.ip().toString(), node.tcpPort());
             if (!member.equals(localMember)) {
                 remoteMembers.add(member);
             }
         }
-        clusterConfig.addRemoteMembers(remoteMembers);
 
-        log.info("Starting cluster with Local:[{}], Remote:{}", localMember, remoteMembers);
+        if (remoteMembers.isEmpty()) {
+            log.info("This node is the only node in the cluster.  "
+                    + "Waiting for others to show up.");
+            // FIXME: hack trying to relax cases forming multiple consensus rings.
+            // add seed node configuration to avoid this
 
+            // If the node is alone on it's own, wait some time
+            // hoping other will come up soon
+            try {
+                if (!clusterEventLatch.await(120, TimeUnit.SECONDS)) {
+                    log.info("Starting as single node cluster");
+                }
+            } catch (InterruptedException e) {
+                log.info("Interrupted waiting for others", e);
+            }
+        }
 
-        // Create the cluster.
-        TcpCluster cluster = new TcpCluster(clusterConfig);
+        final TcpCluster cluster;
+        synchronized (clusterConfig) {
+            clusterConfig.addRemoteMembers(remoteMembers);
+
+            // Create the cluster.
+            cluster = new TcpCluster(clusterConfig);
+        }
+        log.info("Starting cluster: {}", cluster);
+
 
         StateMachine stateMachine = new DatabaseStateMachine();
         // FIXME resolve Chronicle + OSGi issue
@@ -207,17 +234,24 @@
             case INSTANCE_ACTIVATED:
             case INSTANCE_ADDED:
                 log.info("{} was added to the cluster", tcpMember);
-                clusterConfig.addRemoteMember(tcpMember);
+                synchronized (clusterConfig) {
+                    clusterConfig.addRemoteMember(tcpMember);
+                }
                 break;
             case INSTANCE_DEACTIVATED:
             case INSTANCE_REMOVED:
                 log.info("{} was removed from the cluster", tcpMember);
-                clusterConfig.removeRemoteMember(tcpMember);
+                synchronized (clusterConfig) {
+                    clusterConfig.removeRemoteMember(tcpMember);
+                }
                 break;
             default:
                 break;
             }
-            log.info("Current cluster: {}", clusterConfig.getMembers());
+            if (copycat != null) {
+                log.debug("Current cluster: {}", copycat.cluster());
+            }
+            clusterEventLatch.countDown();
         }
 
     }
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 ad6773e..2822f25 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
@@ -1,5 +1,7 @@
 package org.onlab.onos.store.service.impl;
 
+import static org.slf4j.LoggerFactory.getLogger;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -16,6 +18,7 @@
 import org.onlab.onos.store.service.WriteRequest;
 import org.onlab.onos.store.service.WriteResult;
 import org.onlab.util.KryoNamespace;
+import org.slf4j.Logger;
 
 import com.google.common.collect.Maps;
 
@@ -28,6 +31,8 @@
  */
 public class DatabaseStateMachine implements StateMachine {
 
+    private final Logger log = getLogger(getClass());
+
     public static final KryoSerializer SERIALIZER = new KryoSerializer() {
         @Override
         protected void setupKryoPool() {
@@ -161,7 +166,7 @@
         try {
             return SERIALIZER.encode(state);
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("Snapshot serialization error", e);
             return null;
         }
     }
@@ -171,7 +176,7 @@
         try {
             this.state = SERIALIZER.decode(data);
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("Snapshot deserialization error", e);
         }
     }
 }
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
index e0348ff..4cba9f0 100644
--- a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
@@ -103,6 +103,20 @@
 
 public final class KryoNamespaces {
 
+    public static final KryoNamespace BASIC = KryoNamespace.newBuilder()
+            .register(ImmutableMap.class, new ImmutableMapSerializer())
+            .register(ImmutableList.class, new ImmutableListSerializer())
+            .register(ImmutableSet.class, new ImmutableSetSerializer())
+            .register(
+                    ArrayList.class,
+                    Arrays.asList().getClass(),
+                    HashMap.class,
+                    HashSet.class,
+                    LinkedList.class,
+                    byte[].class
+                    )
+            .build();
+
     /**
      * KryoNamespace which can serialize ON.lab misc classes.
      */
@@ -123,19 +137,8 @@
      */
     public static final KryoNamespace API = KryoNamespace.newBuilder()
             .register(MISC)
-            .register(ImmutableMap.class, new ImmutableMapSerializer())
-            .register(ImmutableList.class, new ImmutableListSerializer())
-            .register(ImmutableSet.class, new ImmutableSetSerializer())
+            .register(BASIC)
             .register(
-                    //
-                    ArrayList.class,
-                    Arrays.asList().getClass(),
-                    HashMap.class,
-                    HashSet.class,
-                    LinkedList.class,
-                    byte[].class,
-                    //
-                    //
                     ControllerNode.State.class,
                     Device.Type.class,
                     Port.Type.class,
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 427a23f..375fe6b 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -407,7 +407,8 @@
                     height: this.height,
                     uid: this.uid,
                     setRadio: this.setRadio,
-                    setKeys: this.setKeys
+                    setKeys: this.setKeys,
+                    dataLoadError: this.dataLoadError
                 }
             },
 
@@ -498,6 +499,16 @@
 
             uid: function (id) {
                 return makeUid(this, id);
+            },
+
+            // TODO : implement custom dialogs (don't use alerts)
+
+            dataLoadError: function (err, url) {
+                var msg = 'Data Load Error\n\n' +
+                    err.status + ' -- ' + err.statusText + '\n\n' +
+                    'relative-url: "' + url + '"\n\n' +
+                    'complete-url: "' + err.responseURL + '"';
+                alert(msg);
             }
 
             // TODO: consider schedule, clearTimer, etc.
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 88fcd94..eee9244 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -24,3 +24,6 @@
     opacity: 0.5;
 }
 
+svg .node {
+    fill: #03c;
+}
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 7ac9adc..f5e1792 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -56,12 +56,24 @@
             opt: 'img/opt.png'
         },
         force: {
-            marginLR: 20,
-            marginTB: 20,
+            note: 'node.class or link.class is used to differentiate',
+            linkDistance: {
+                infra: 200,
+                host: 40
+            },
+            linkStrength: {
+                infra: 1.0,
+                host: 1.0
+            },
+            charge: {
+                device: -400,
+                host: -100
+            },
+            pad: 20,
             translate: function() {
                 return 'translate(' +
-                    config.force.marginLR + ',' +
-                    config.force.marginTB + ')';
+                    config.force.pad + ',' +
+                    config.force.pad + ')';
             }
         }
     };
@@ -94,7 +106,11 @@
     // D3 selections
     var svg,
         bgImg,
-        topoG;
+        topoG,
+        nodeG,
+        linkG,
+        node,
+        link;
 
     // ==============================
     // For Debugging / Development
@@ -175,22 +191,145 @@
     // ==============================
     // Private functions
 
-    // set the size of the given element to that of the view
-    function setSize(el, view) {
+    // set the size of the given element to that of the view (reduced if padded)
+    function setSize(el, view, pad) {
+        var padding = pad ? pad * 2 : 0;
         el.attr({
-            width: view.width(),
-            height: view.height()
+            width: view.width() - padding,
+            height: view.height() - padding
         });
     }
 
-
     function getNetworkData(view) {
         var url = getTopoUrl();
 
-        // TODO ...
-
+        console.log('Fetching JSON: ' + url);
+        d3.json(url, function(err, data) {
+            if (err) {
+                view.dataLoadError(err, url);
+            } else {
+                network.data = data;
+                drawNetwork(view);
+            }
+        });
     }
 
+    function drawNetwork(view) {
+        preprocessData(view);
+        updateLayout(view);
+    }
+
+    function preprocessData(view) {
+        var w = view.width(),
+            h = view.height(),
+            hDevice = h * 0.6,
+            hHost = h * 0.3,
+            data = network.data,
+            deviceLayout = computeInitLayout(w, hDevice, data.devices.length),
+            hostLayout = computeInitLayout(w, hHost, data.hosts.length);
+
+        network.lookup = {};
+        network.nodes = [];
+        network.links = [];
+        // we created new arrays, so need to set the refs in the force layout
+        network.force.nodes(network.nodes);
+        network.force.links(network.links);
+
+        // let's just start with the nodes
+
+        // note that both 'devices' and 'hosts' get mapped into the nodes array
+        function makeNode(d, cls, layout) {
+            var node = {
+                    id: d.id,
+                    labels: d.labels,
+                    class: cls,
+                    icon: cls,
+                    type: d.type,
+                    x: layout.x(),
+                    y: layout.y()
+                };
+            network.lookup[d.id] = node;
+            network.nodes.push(node);
+        }
+
+        // first the devices...
+        network.data.devices.forEach(function (d) {
+            makeNode(d, 'device', deviceLayout);
+        });
+
+        // then the hosts...
+        network.data.hosts.forEach(function (d) {
+            makeNode(d, 'host', hostLayout);
+        });
+
+        // TODO: process links
+    }
+
+    function computeInitLayout(w, h, n) {
+        var maxdw = 60,
+            compdw, dw, ox, layout;
+
+        if (n < 2) {
+            layout = { ox: w/2, dw: 0 }
+        } else {
+            compdw = (0.8 * w) / (n - 1);
+            dw = Math.min(maxdw, compdw);
+            ox = w/2 - ((n - 1)/2 * dw);
+            layout = { ox: ox, dw: dw }
+        }
+
+        layout.i = 0;
+
+        layout.x = function () {
+            var x = layout.ox + layout.i*layout.dw;
+            layout.i++;
+            return x;
+        };
+
+        layout.y = function () {
+            return h;
+        };
+
+        return layout;
+    }
+
+    function linkId(d) {
+        return d.source.id + '~' + d.target.id;
+    }
+
+    function nodeId(d) {
+        return d.id;
+    }
+
+    function updateLayout(view) {
+        link = link.data(network.force.links(), linkId);
+        link.enter().append('line')
+            .attr('class', 'link');
+        link.exit().remove();
+
+        node = node.data(network.force.nodes(), nodeId);
+        node.enter().append('circle')
+            .attr('id', function (d) { return 'nodeId-' + d.id; })
+            .attr('class', function (d) { return 'node'; })
+            .attr('r', 12);
+
+        network.force.start();
+    }
+
+
+    function tick() {
+        node.attr({
+            cx: function(d) { return d.x; },
+            cy: function(d) { return d.y; }
+        });
+
+        link.attr({
+            x1: function (d) { return d.source.x; },
+            y1: function (d) { return d.source.y; },
+            x2: function (d) { return d.target.x; },
+            y2: function (d) { return d.target.y; }
+        });
+    }
 
     // ==============================
     // View life-cycle callbacks
@@ -199,15 +338,15 @@
         var w = view.width(),
             h = view.height(),
             idBg = view.uid('bg'),
-            showBg = config.options.showBackground ? 'visible' : 'hidden';
+            showBg = config.options.showBackground ? 'visible' : 'hidden',
+            fcfg = config.force,
+            fpad = fcfg.pad,
+            forceDim = [w - 2*fpad, h - 2*fpad];
 
         // NOTE: view.$div is a D3 selection of the view's div
         svg = view.$div.append('svg');
         setSize(svg, view);
 
-        topoG = svg.append('g')
-            .attr('transform', config.force.translate());
-
         // load the background image
         bgImg = svg.append('svg:image')
             .attr({
@@ -219,6 +358,28 @@
             .style({
                 visibility: showBg
             });
+
+        // group for the topology
+        topoG = svg.append('g')
+            .attr('transform', fcfg.translate());
+
+        // subgroups for links and nodes
+        linkG = topoG.append('g').attr('id', 'links');
+        nodeG = topoG.append('g').attr('id', 'nodes');
+
+        // selection of nodes and links
+        link = linkG.selectAll('.link');
+        node = nodeG.selectAll('.node');
+
+        // set up the force layout
+        network.force = d3.layout.force()
+            .size(forceDim)
+            .nodes(network.nodes)
+            .links(network.links)
+            .charge(function (d) { return fcfg.charge[d.class]; })
+            .linkDistance(function (d) { return fcfg.linkDistance[d.class]; })
+            .linkStrength(function (d) { return fcfg.linkStrength[d.class]; })
+            .on('tick', tick);
     }