Consolidating null providers and making them fully configurable and integrated with the ConfigProvider to allow arbitrary topologies.

Change-Id: I899e27a9771af4013a3ce6da7f683a4927ffb438
diff --git a/web/api/src/main/java/org/onosproject/rest/ConfigProvider.java b/web/api/src/main/java/org/onosproject/rest/ConfigProvider.java
index bec1941..3974f17 100644
--- a/web/api/src/main/java/org/onosproject/rest/ConfigProvider.java
+++ b/web/api/src/main/java/org/onosproject/rest/ConfigProvider.java
@@ -16,6 +16,11 @@
 package org.onosproject.rest;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.Lists;
+import org.onlab.packet.ChassisId;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DefaultAnnotations;
 import org.onosproject.net.Device;
@@ -30,9 +35,12 @@
 import org.onosproject.net.device.DefaultDeviceDescription;
 import org.onosproject.net.device.DefaultPortDescription;
 import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
 import org.onosproject.net.device.DeviceProvider;
 import org.onosproject.net.device.DeviceProviderRegistry;
 import org.onosproject.net.device.DeviceProviderService;
+import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.device.PortDescription;
 import org.onosproject.net.host.DefaultHostDescription;
 import org.onosproject.net.host.HostProvider;
@@ -43,10 +51,8 @@
 import org.onosproject.net.link.LinkProviderRegistry;
 import org.onosproject.net.link.LinkProviderService;
 import org.onosproject.net.provider.ProviderId;
-import org.onlab.packet.ChassisId;
-import org.onlab.packet.IpAddress;
-import org.onlab.packet.MacAddress;
-import org.onlab.packet.VlanId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.net.URI;
 import java.util.ArrayList;
@@ -54,37 +60,60 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onosproject.net.DeviceId.deviceId;
 import static org.onosproject.net.PortNumber.portNumber;
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_AVAILABILITY_CHANGED;
 
 /**
  * Provider of devices and links parsed from a JSON configuration structure.
  */
 class ConfigProvider implements DeviceProvider, LinkProvider, HostProvider {
 
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
     private static final ProviderId PID =
             new ProviderId("cfg", "org.onosproject.rest", true);
 
+    private static final String UNKNOWN = "unknown";
+
+    private CountDownLatch deviceLatch;
+
     private final JsonNode cfg;
+    private final DeviceService deviceService;
+
     private final DeviceProviderRegistry deviceProviderRegistry;
     private final LinkProviderRegistry linkProviderRegistry;
     private final HostProviderRegistry hostProviderRegistry;
 
+    private DeviceProviderService deviceProviderService;
+    private LinkProviderService linkProviderService;
+    private HostProviderService hostProviderService;
+
+    private DeviceListener deviceEventCounter = new DeviceEventCounter();
+    private List<ConnectPoint> connectPoints = Lists.newArrayList();
+
     /**
      * Creates a new configuration provider.
      *
      * @param cfg                    JSON configuration
+     * @param deviceService          device service
      * @param deviceProviderRegistry device provider registry
      * @param linkProviderRegistry   link provider registry
      * @param hostProviderRegistry   host provider registry
      */
     ConfigProvider(JsonNode cfg,
+                   DeviceService deviceService,
                    DeviceProviderRegistry deviceProviderRegistry,
                    LinkProviderRegistry linkProviderRegistry,
                    HostProviderRegistry hostProviderRegistry) {
         this.cfg = checkNotNull(cfg, "Configuration cannot be null");
+        this.deviceService = checkNotNull(deviceService, "Device service cannot be null");
         this.deviceProviderRegistry = checkNotNull(deviceProviderRegistry, "Device provider registry cannot be null");
         this.linkProviderRegistry = checkNotNull(linkProviderRegistry, "Link provider registry cannot be null");
         this.hostProviderRegistry = checkNotNull(hostProviderRegistry, "Host provider registry cannot be null");
@@ -94,56 +123,74 @@
      * Parses the given JSON and provides links as configured.
      */
     void parse() {
-        parseDevices();
-        parseLinks();
-        parseHosts();
+        try {
+            register();
+            parseDevices();
+            parseLinks();
+            parseHosts();
+            addMissingPorts();
+        } finally {
+            unregister();
+        }
+    }
+
+    private void register() {
+        deviceProviderService = deviceProviderRegistry.register(this);
+        linkProviderService = linkProviderRegistry.register(this);
+        hostProviderService = hostProviderRegistry.register(this);
+    }
+
+    private void unregister() {
+        deviceProviderRegistry.unregister(this);
+        linkProviderRegistry.unregister(this);
+        hostProviderRegistry.unregister(this);
     }
 
     // Parses the given JSON and provides devices.
     private void parseDevices() {
         try {
-            DeviceProviderService dps = deviceProviderRegistry.register(this);
             JsonNode nodes = cfg.get("devices");
             if (nodes != null) {
+                prepareForDeviceEvents(nodes.size());
                 for (JsonNode node : nodes) {
-                    parseDevice(dps, node);
+                    parseDevice(node);
                 }
             }
         } finally {
-            deviceProviderRegistry.unregister(this);
+            waitForDeviceEvents();
         }
     }
 
     // Parses the given node with device data and supplies the device.
-    private void parseDevice(DeviceProviderService dps, JsonNode node) {
+    private void parseDevice(JsonNode node) {
         URI uri = URI.create(get(node, "uri"));
-        Device.Type type = Device.Type.valueOf(get(node, "type"));
-        String mfr = get(node, "mfr");
-        String hw = get(node, "hw");
-        String sw = get(node, "sw");
-        String serial = get(node, "serial");
-        ChassisId cid = new ChassisId(get(node, "mac"));
+        Device.Type type = Device.Type.valueOf(get(node, "type", "SWITCH"));
+        String mfr = get(node, "mfr", UNKNOWN);
+        String hw = get(node, "hw", UNKNOWN);
+        String sw = get(node, "sw", UNKNOWN);
+        String serial = get(node, "serial", UNKNOWN);
+        ChassisId cid = new ChassisId(get(node, "mac", "000000000000"));
         SparseAnnotations annotations = annotations(node.get("annotations"));
 
         DeviceDescription desc =
                 new DefaultDeviceDescription(uri, type, mfr, hw, sw, serial,
                                              cid, annotations);
         DeviceId deviceId = deviceId(uri);
-        dps.deviceConnected(deviceId, desc);
+        deviceProviderService.deviceConnected(deviceId, desc);
 
         JsonNode ports = node.get("ports");
         if (ports != null) {
-            parsePorts(dps, deviceId, ports);
+            parsePorts(deviceId, ports);
         }
     }
 
     // Parses the given node with list of device ports.
-    private void parsePorts(DeviceProviderService dps, DeviceId deviceId, JsonNode nodes) {
+    private void parsePorts(DeviceId deviceId, JsonNode nodes) {
         List<PortDescription> ports = new ArrayList<>();
         for (JsonNode node : nodes) {
             ports.add(parsePort(node));
         }
-        dps.updatePorts(deviceId, ports);
+        deviceProviderService.updatePorts(deviceId, ports);
     }
 
     // Parses the given node with port information.
@@ -156,43 +203,41 @@
 
     // Parses the given JSON and provides links as configured.
     private void parseLinks() {
-        try {
-            LinkProviderService lps = linkProviderRegistry.register(this);
-            JsonNode nodes = cfg.get("links");
-            if (nodes != null) {
-                for (JsonNode node : nodes) {
-                    parseLink(lps, node, false);
-                    if (!node.has("halfplex")) {
-                        parseLink(lps, node, true);
-                    }
+        JsonNode nodes = cfg.get("links");
+        if (nodes != null) {
+            for (JsonNode node : nodes) {
+                parseLink(node, false);
+                if (!node.has("halfplex")) {
+                    parseLink(node, true);
                 }
             }
-        } finally {
-            linkProviderRegistry.unregister(this);
         }
     }
 
     // Parses the given node with link data and supplies the link.
-    private void parseLink(LinkProviderService lps, JsonNode node, boolean reverse) {
+    private void parseLink(JsonNode node, boolean reverse) {
         ConnectPoint src = connectPoint(get(node, "src"));
         ConnectPoint dst = connectPoint(get(node, "dst"));
-        Link.Type type = Link.Type.valueOf(get(node, "type"));
+        Link.Type type = Link.Type.valueOf(get(node, "type", "DIRECT"));
         SparseAnnotations annotations = annotations(node.get("annotations"));
 
         DefaultLinkDescription desc = reverse ?
                 new DefaultLinkDescription(dst, src, type, annotations) :
                 new DefaultLinkDescription(src, dst, type, annotations);
-        lps.linkDetected(desc);
+        linkProviderService.linkDetected(desc);
+
+        connectPoints.add(src);
+        connectPoints.add(dst);
     }
 
     // Parses the given JSON and provides hosts as configured.
     private void parseHosts() {
         try {
-            HostProviderService hps = hostProviderRegistry.register(this);
             JsonNode nodes = cfg.get("hosts");
             if (nodes != null) {
                 for (JsonNode node : nodes) {
-                    parseHost(hps, node);
+                    parseHost(node);
+                    parseHost(node); // FIXME: hack to make sure host positions take
                 }
             }
         } finally {
@@ -201,14 +246,14 @@
     }
 
     // Parses the given node with host data and supplies the host.
-    private void parseHost(HostProviderService hps, JsonNode node) {
+    private void parseHost(JsonNode node) {
         MacAddress mac = MacAddress.valueOf(get(node, "mac"));
-        VlanId vlanId = VlanId.vlanId(node.get("vlan").shortValue());
+        VlanId vlanId = VlanId.vlanId((short) node.get("vlan").asInt(VlanId.UNTAGGED));
         HostId hostId = HostId.hostId(mac, vlanId);
         SparseAnnotations annotations = annotations(node.get("annotations"));
         HostLocation location = new HostLocation(connectPoint(get(node, "location")), 0);
 
-        String[] ipStrings = get(node, "ip").split(",");
+        String[] ipStrings = get(node, "ip", "").split(",");
         Set<IpAddress> ips = new HashSet<>();
         for (String ip : ipStrings) {
             ips.add(IpAddress.valueOf(ip.trim()));
@@ -216,9 +261,45 @@
 
         DefaultHostDescription desc =
                 new DefaultHostDescription(mac, vlanId, location, ips, annotations);
-        hps.hostDetected(hostId, desc);
+        hostProviderService.hostDetected(hostId, desc);
+
+        connectPoints.add(location);
     }
 
+    // Adds any missing device ports for configured links and host locations.
+    private void addMissingPorts() {
+        deviceService.getDevices().forEach(this::addMissingPorts);
+    }
+
+    // Adds any missing device ports.
+    private void addMissingPorts(Device device) {
+        List<Port> ports = deviceService.getPorts(device.id());
+        Set<ConnectPoint> existing = ports.stream()
+                .map(p -> new ConnectPoint(device.id(), p.number()))
+                .collect(Collectors.toSet());
+        Set<ConnectPoint> missing = connectPoints.stream()
+                .filter(cp -> !existing.contains(cp))
+                .collect(Collectors.toSet());
+
+        if (!missing.isEmpty()) {
+            List<PortDescription> newPorts = Lists.newArrayList();
+            ports.forEach(p -> newPorts.add(description(p)));
+            missing.forEach(cp -> newPorts.add(description(cp)));
+            deviceProviderService.updatePorts(device.id(), newPorts);
+        }
+    }
+
+    // Creates a port description from the specified port.
+    private PortDescription description(Port p) {
+        return new DefaultPortDescription(p.number(), p.isEnabled(), p.type(), p.portSpeed());
+    }
+
+    // Creates a port description from the specified connection point.
+    private PortDescription description(ConnectPoint cp) {
+        return new DefaultPortDescription(cp.port(), true);
+    }
+
+
     // Produces set of annotations from the given JSON node.
     private SparseAnnotations annotations(JsonNode node) {
         if (node == null) {
@@ -246,12 +327,18 @@
         return node.path(name).asText();
     }
 
-    @Override
-    public void triggerProbe(DeviceId deviceId) {
+    // Returns string form of the named property in the given JSON object.
+    private String get(JsonNode node, String name, String defaultValue) {
+        return node.path(name).asText(defaultValue);
     }
 
     @Override
     public void roleChanged(DeviceId device, MastershipRole newRole) {
+        deviceProviderService.receivedRoleReply(device, newRole, newRole);
+    }
+
+    @Override
+    public void triggerProbe(DeviceId deviceId) {
     }
 
     @Override
@@ -265,6 +352,40 @@
 
     @Override
     public boolean isReachable(DeviceId device) {
-        return false;
+        return true;
     }
+
+    /**
+     * Prepares to count device added/available/removed events.
+     *
+     * @param count number of events to count
+     */
+    protected void prepareForDeviceEvents(int count) {
+        deviceLatch = new CountDownLatch(count);
+        deviceService.addListener(deviceEventCounter);
+    }
+
+    /**
+     * Waits for all expected device added/available/removed events.
+     */
+    protected void waitForDeviceEvents() {
+        try {
+            deviceLatch.await(2, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            log.warn("Device events did not arrive in time");
+        }
+        deviceService.removeListener(deviceEventCounter);
+    }
+
+    // Counts down number of device added/available/removed events.
+    private class DeviceEventCounter implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            DeviceEvent.Type type = event.type();
+            if (type == DEVICE_ADDED || type == DEVICE_AVAILABILITY_CHANGED) {
+                deviceLatch.countDown();
+            }
+        }
+    }
+
 }