ONOS-2186 - GUI Topo Overlay - (WIP)
- moved TopoUtils, NodeSelection, BiLink and Map (and Base derivatives) to core API.

Change-Id: I105f6df6508b1597ffde19fe7e360d3775abf250
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/BaseLink.java b/core/api/src/main/java/org/onosproject/ui/topo/BaseLink.java
new file mode 100644
index 0000000..c37c129
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/BaseLink.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015 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.onosproject.ui.topo;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+
+/**
+ * A simple concrete implementation of a {@link BiLink}.
+ * Note that this implementation does not generate any link highlights.
+ */
+public class BaseLink extends BiLink {
+
+    /**
+     * Constructs a base link for the given key and initial link.
+     *
+     * @param key  canonical key for this base link
+     * @param link first link
+     */
+    public BaseLink(LinkKey key, Link link) {
+        super(key, link);
+    }
+
+    @Override
+    public LinkHighlight highlight(Enum<?> type) {
+        return null;
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/BaseLinkMap.java b/core/api/src/main/java/org/onosproject/ui/topo/BaseLinkMap.java
new file mode 100644
index 0000000..720eca4
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/BaseLinkMap.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015 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.onosproject.ui.topo;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+
+/**
+ * Collection of {@link BaseLink}s.
+ */
+public class BaseLinkMap extends BiLinkMap<BaseLink> {
+    @Override
+    public BaseLink create(LinkKey key, Link link) {
+        return new BaseLink(key, link);
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/BiLink.java b/core/api/src/main/java/org/onosproject/ui/topo/BiLink.java
new file mode 100644
index 0000000..8c95e15
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/BiLink.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2015 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.onosproject.ui.topo;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Representation of a link and its inverse, as a partial implementation.
+ * <p>
+ * Subclasses will decide how to generate the link highlighting (coloring
+ * and labeling) for the topology view.
+ */
+public abstract class BiLink {
+
+    private final LinkKey key;
+    private final Link one;
+    private Link two;
+
+    /**
+     * Constructs a bi-link for the given key and initial link. It is expected
+     * that the caller will have used {@link TopoUtils#canonicalLinkKey(Link)}
+     * to generate the key.
+     *
+     * @param key canonical key for this bi-link
+     * @param link first link
+     */
+    public BiLink(LinkKey key, Link link) {
+        this.key = checkNotNull(key);
+        this.one = checkNotNull(link);
+    }
+
+    /**
+     * Sets the second link for this bi-link.
+     *
+     * @param link second link
+     */
+    public void setOther(Link link) {
+        this.two = checkNotNull(link);
+    }
+
+    /**
+     * Returns the link identifier in the form expected on the Topology View
+     * in the web client.
+     *
+     * @return link identifier
+     */
+    public String linkId() {
+        return TopoUtils.compactLinkString(one);
+    }
+
+    /**
+     * Returns the key for this bi-link.
+     *
+     * @return the key
+     */
+    public LinkKey key() {
+        return key;
+    }
+
+    /**
+     * Returns the first link in this bi-link.
+     *
+     * @return the first link
+     */
+    public Link one() {
+        return one;
+    }
+
+    /**
+     * Returns the second link in this bi-link.
+     *
+     * @return the second link
+     */
+    public Link two() {
+        return two;
+    }
+
+    /**
+     * Returns the link highlighting to use, based on this bi-link's current
+     * state.
+     *
+     * @param type optional highlighting type parameter
+     * @return link highlighting model
+     */
+    public abstract LinkHighlight highlight(Enum<?> type);
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/BiLinkMap.java b/core/api/src/main/java/org/onosproject/ui/topo/BiLinkMap.java
new file mode 100644
index 0000000..66f0f8f
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/BiLinkMap.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2015 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.onosproject.ui.topo;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Represents a collection of {@link BiLink} concrete classes. These maps
+ * are used to collate a set of unidirectional {@link Link}s into a smaller
+ * set of bi-directional {@link BiLink} derivatives.
+ * <p>
+ * @param <B> the type of bi-link subclass
+ */
+public abstract class BiLinkMap<B extends BiLink> {
+
+    private final Map<LinkKey, B> map = new HashMap<>();
+
+    /**
+     * Creates a new instance of a bi-link. Concrete subclasses should
+     * instantiate and return the appropriate bi-link subclass.
+     *
+     * @param key the link key
+     * @param link the initial link
+     * @return a new instance
+     */
+    public abstract B create(LinkKey key, Link link);
+
+    /**
+     * Adds the given link to our collection, returning the corresponding
+     * bi-link (creating one if needed necessary).
+     *
+     * @param link the link to add to the collection
+     * @return the corresponding bi-link wrapper
+     */
+    public B add(Link link) {
+        LinkKey key = TopoUtils.canonicalLinkKey(checkNotNull(link));
+        B blink = map.get(key);
+        if (blink == null) {
+            // no bi-link yet exists for this link
+            blink = create(key, link);
+            map.put(key, blink);
+        } else {
+            // we have a bi-link for this link.
+            if (!blink.one().equals(link)) {
+                blink.setOther(link);
+            }
+        }
+        return blink;
+    }
+
+    /**
+     * Returns the bi-link instances in the collection.
+     *
+     * @return the bi-links in this map
+     */
+    public Collection<B> biLinks() {
+        return map.values();
+    }
+
+    /**
+     * Returns the number of bi-links in the collection.
+     *
+     * @return number of bi-links
+     */
+    public int size() {
+        return map.size();
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/NodeSelection.java b/core/api/src/main/java/org/onosproject/ui/topo/NodeSelection.java
new file mode 100644
index 0000000..cefbf03
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/NodeSelection.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2015 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.onosproject.ui.topo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.net.Device;
+import org.onosproject.net.Host;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.ui.JsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.HostId.hostId;
+
+/**
+ * Encapsulates a selection of devices and/or hosts from the topology view.
+ */
+public class NodeSelection {
+
+    private static final Logger log =
+            LoggerFactory.getLogger(NodeSelection.class);
+
+    private static final String IDS = "ids";
+    private static final String HOVER = "hover";
+
+    private final DeviceService deviceService;
+    private final HostService hostService;
+
+    private final Set<String> ids;
+    private final String hover;
+
+    private final Set<Device> devices = new HashSet<>();
+    private final Set<Host> hosts = new HashSet<>();
+
+    /**
+     * Creates a node selection entity, from the given payload, using the
+     * supplied device and host services.
+     *
+     * @param payload message payload
+     * @param deviceService device service
+     * @param hostService host service
+     */
+    public NodeSelection(ObjectNode payload,
+                         DeviceService deviceService,
+                         HostService hostService) {
+        this.deviceService = deviceService;
+        this.hostService = hostService;
+
+        ids = extractIds(payload);
+        hover = extractHover(payload);
+
+        Set<String> unmatched = findDevices(ids);
+        unmatched = findHosts(unmatched);
+        if (unmatched.size() > 0) {
+            log.debug("Skipping unmatched IDs {}", unmatched);
+        }
+
+        if (!isNullOrEmpty(hover)) {
+            unmatched = new HashSet<>();
+            unmatched.add(hover);
+            unmatched = findDevices(unmatched);
+            unmatched = findHosts(unmatched);
+            if (unmatched.size() > 0) {
+                log.debug("Skipping unmatched HOVER {}", unmatched);
+            }
+        }
+    }
+
+    /**
+     * Returns a view of the selected devices.
+     *
+     * @return selected devices
+     */
+    public Set<Device> devices() {
+        return Collections.unmodifiableSet(devices);
+    }
+
+    /**
+     * Returns a view of the selected hosts.
+     *
+     * @return selected hosts
+     */
+    public Set<Host> hosts() {
+        return Collections.unmodifiableSet(hosts);
+    }
+
+    /**
+     * Returns true if nothing is selected.
+     *
+     * @return true if nothing selected
+     */
+    public boolean none() {
+        return devices().size() == 0 && hosts().size() == 0;
+    }
+
+    @Override
+    public String toString() {
+        return "NodeSelection{" +
+                "ids=" + ids +
+                ", hover='" + hover + '\'' +
+                ", #devices=" + devices.size() +
+                ", #hosts=" + hosts.size() +
+                '}';
+    }
+
+    // == helper methods
+
+    private Set<String> extractIds(ObjectNode payload) {
+        ArrayNode array = (ArrayNode) payload.path(IDS);
+        if (array == null || array.size() == 0) {
+            return Collections.emptySet();
+        }
+
+        Set<String> ids = new HashSet<>();
+        for (JsonNode node : array) {
+            ids.add(node.asText());
+        }
+        return ids;
+    }
+
+    private String extractHover(ObjectNode payload) {
+        return JsonUtils.string(payload, HOVER);
+    }
+
+    private Set<String> findDevices(Set<String> ids) {
+        Set<String> unmatched = new HashSet<>();
+        Device device;
+
+        for (String id : ids) {
+            try {
+                device = deviceService.getDevice(deviceId(id));
+                if (device != null) {
+                    devices.add(device);
+                } else {
+                    log.debug("Device with ID {} not found", id);
+                }
+            } catch (IllegalArgumentException e) {
+                unmatched.add(id);
+            }
+        }
+        return unmatched;
+    }
+
+    private Set<String> findHosts(Set<String> ids) {
+        Set<String> unmatched = new HashSet<>();
+        Host host;
+
+        for (String id : ids) {
+            try {
+                host = hostService.getHost(hostId(id));
+                if (host != null) {
+                    hosts.add(host);
+                } else {
+                    log.debug("Host with ID {} not found", id);
+                }
+            } catch (IllegalArgumentException e) {
+                unmatched.add(id);
+            }
+        }
+        return unmatched;
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/TopoUtils.java b/core/api/src/main/java/org/onosproject/ui/topo/TopoUtils.java
new file mode 100644
index 0000000..f92d579
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/TopoUtils.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2015 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.onosproject.ui.topo;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+
+import java.text.DecimalFormat;
+
+import static org.onosproject.net.LinkKey.linkKey;
+
+/**
+ * Utility methods for helping out with formatting data for the Topology View
+ * in the web client.
+ */
+public final class TopoUtils {
+
+    // explicit decision made to not 'javadoc' these self explanatory constants
+    public static final double KILO = 1024;
+    public static final double MEGA = 1024 * KILO;
+    public static final double GIGA = 1024 * MEGA;
+
+    public static final String GBITS_UNIT = "Gb";
+    public static final String MBITS_UNIT = "Mb";
+    public static final String KBITS_UNIT = "Kb";
+    public static final String BITS_UNIT = "b";
+    public static final String GBYTES_UNIT = "GB";
+    public static final String MBYTES_UNIT = "MB";
+    public static final String KBYTES_UNIT = "KB";
+    public static final String BYTES_UNIT = "B";
+
+
+    private static final DecimalFormat DF2 = new DecimalFormat("#,###.##");
+
+    private static final String COMPACT = "%s/%s-%s/%s";
+    private static final String EMPTY = "";
+    private static final String SPACE = " ";
+    private static final String PER_SEC = "ps";
+    private static final String FLOW = "flow";
+    private static final String FLOWS = "flows";
+
+    // non-instantiable
+    private TopoUtils() { }
+
+    /**
+     * Returns a compact identity for the given link, in the form
+     * used to identify links in the Topology View on the client.
+     *
+     * @param link link
+     * @return compact link identity
+     */
+    public static String compactLinkString(Link link) {
+        return String.format(COMPACT, link.src().elementId(), link.src().port(),
+                             link.dst().elementId(), link.dst().port());
+    }
+
+    /**
+     * Produces a canonical link key, that is, one that will match both a link
+     * and its inverse.
+     *
+     * @param link the link
+     * @return canonical key
+     */
+    public static LinkKey canonicalLinkKey(Link link) {
+        String sn = link.src().elementId().toString();
+        String dn = link.dst().elementId().toString();
+        return sn.compareTo(dn) < 0 ?
+                linkKey(link.src(), link.dst()) : linkKey(link.dst(), link.src());
+    }
+
+    /**
+     * Returns human readable count of bytes, to be displayed as a label.
+     *
+     * @param bytes number of bytes
+     * @return formatted byte count
+     */
+    public static String formatBytes(long bytes) {
+        String unit;
+        double value;
+        if (bytes > GIGA) {
+            value = bytes / GIGA;
+            unit = GBYTES_UNIT;
+        } else if (bytes > MEGA) {
+            value = bytes / MEGA;
+            unit = MBYTES_UNIT;
+        } else if (bytes > KILO) {
+            value = bytes / KILO;
+            unit = KBYTES_UNIT;
+        } else {
+            value = bytes;
+            unit = BYTES_UNIT;
+        }
+        return DF2.format(value) + SPACE + unit;
+    }
+
+    /**
+     * Returns human readable bit rate, to be displayed as a label.
+     *
+     * @param bytes bytes per second
+     * @return formatted bits per second
+     */
+    public static String formatBitRate(long bytes) {
+        String unit;
+        double value;
+
+        //Convert to bits
+        long bits = bytes * 8;
+        if (bits > GIGA) {
+            value = bits / GIGA;
+            unit = GBITS_UNIT;
+
+            // NOTE: temporary hack to clip rate at 10.0 Gbps
+            //  Added for the CORD Fabric demo at ONS 2015
+            // TODO: provide a more elegant solution to this issue
+            if (value > 10.0) {
+                value = 10.0;
+            }
+
+        } else if (bits > MEGA) {
+            value = bits / MEGA;
+            unit = MBITS_UNIT;
+        } else if (bits > KILO) {
+            value = bits / KILO;
+            unit = KBITS_UNIT;
+        } else {
+            value = bits;
+            unit = BITS_UNIT;
+        }
+        return DF2.format(value) + SPACE + unit + PER_SEC;
+    }
+
+    /**
+     * Returns human readable flow count, to be displayed as a label.
+     *
+     * @param flows number of flows
+     * @return formatted flow count
+     */
+    public static String formatFlows(long flows) {
+        if (flows < 1) {
+            return EMPTY;
+        }
+        return String.valueOf(flows) + SPACE + (flows > 1 ? FLOWS : FLOW);
+    }
+}