Enhance the RouteEvent to notify listeners of alternative viable routes.

Traditionally the route event only notified listeners of the best selected
route for a given prefix, but some listeners are interested in all resolved
routes for the prefix.

CORD-905

Change-Id: Ia3e1e3a8e3e825ba894e6835e0860c3ed698d29b
diff --git a/incubator/api/src/main/java/org/onosproject/incubator/net/routing/RouteEvent.java b/incubator/api/src/main/java/org/onosproject/incubator/net/routing/RouteEvent.java
index ef56145..eceed5d 100644
--- a/incubator/api/src/main/java/org/onosproject/incubator/net/routing/RouteEvent.java
+++ b/incubator/api/src/main/java/org/onosproject/incubator/net/routing/RouteEvent.java
@@ -19,6 +19,8 @@
 import org.joda.time.LocalDateTime;
 import org.onosproject.event.AbstractEvent;
 
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Objects;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
@@ -29,6 +31,7 @@
 public class RouteEvent extends AbstractEvent<RouteEvent.Type, ResolvedRoute> {
 
     private final ResolvedRoute prevSubject;
+    private final Collection<ResolvedRoute> alternativeRoutes;
 
     /**
      * Route event type.
@@ -40,6 +43,7 @@
          * <p>
          * The subject of this event should be the route being added.
          * The prevSubject of this event should be null.
+         * </p>
          */
         ROUTE_ADDED,
 
@@ -48,6 +52,7 @@
          * <p>
          * The subject of this event should be the new route.
          * The prevSubject of this event should be the old route.
+         * </p>
          */
         ROUTE_UPDATED,
 
@@ -56,8 +61,21 @@
          * <p>
          * The subject of this event should be the route being removed.
          * The prevSubject of this event should be null.
+         * </p>
          */
-        ROUTE_REMOVED
+        ROUTE_REMOVED,
+
+        /**
+         * The set of alternative routes for the subject's prefix has changed,
+         * but the best route is still the same.
+         * <p>
+         * The subject is the best route for the prefix (which has already been
+         * notified in a previous event).
+         * The prevSubject of this event is null.
+         * The alternatives contains the new set of alternative routes.
+         * </p>
+         */
+        ALTERNATIVE_ROUTES_CHANGED
     }
 
     /**
@@ -67,8 +85,18 @@
      * @param subject event subject
      */
     public RouteEvent(Type type, ResolvedRoute subject) {
-        super(type, subject);
-        this.prevSubject = null;
+        this(type, subject, null, Collections.emptySet());
+    }
+
+    /**
+     * Creates a new route event without specifying previous subject.
+     *
+     * @param type event type
+     * @param subject event subject
+     * @param alternatives alternative routes for subject's prefix
+     */
+    public RouteEvent(Type type, ResolvedRoute subject, Collection<ResolvedRoute> alternatives) {
+        this(type, subject, null, alternatives);
     }
 
     /**
@@ -81,6 +109,8 @@
     protected RouteEvent(Type type, ResolvedRoute subject, long time) {
         super(type, subject, time);
         this.prevSubject = null;
+
+        this.alternativeRoutes = Collections.emptySet();
     }
 
     /**
@@ -91,8 +121,22 @@
      * @param prevSubject previous subject
      */
     public RouteEvent(Type type, ResolvedRoute subject, ResolvedRoute prevSubject) {
+        this(type, subject, prevSubject, Collections.emptySet());
+    }
+
+    /**
+     * Creates a new route event with a previous subject and alternative routes.
+     *
+     * @param type event type
+     * @param subject event subject
+     * @param prevSubject previous subject
+     * @param alternatives alternative routes for subject's prefix
+     */
+    public RouteEvent(Type type, ResolvedRoute subject, ResolvedRoute prevSubject,
+                      Collection<ResolvedRoute> alternatives) {
         super(type, subject);
         this.prevSubject = prevSubject;
+        this.alternativeRoutes = alternatives;
     }
 
     /**
@@ -104,9 +148,18 @@
         return prevSubject;
     }
 
+    /**
+     * Returns the set of alternative routes for the subject's prefix.
+     *
+     * @return alternative routes
+     */
+    public Collection<ResolvedRoute> alternatives() {
+        return alternativeRoutes;
+    }
+
     @Override
     public int hashCode() {
-        return Objects.hash(subject(), type(), prevSubject());
+        return Objects.hash(subject(), type(), prevSubject(), alternativeRoutes);
     }
 
     @Override
@@ -123,7 +176,8 @@
 
         return Objects.equals(this.subject(), that.subject()) &&
                 Objects.equals(this.type(), that.type()) &&
-                Objects.equals(this.prevSubject(), that.prevSubject());
+                Objects.equals(this.prevSubject, that.prevSubject) &&
+                Objects.equals(this.alternativeRoutes, that.alternativeRoutes);
     }
 
     @Override
@@ -132,7 +186,8 @@
                 .add("time", new LocalDateTime(time()))
                 .add("type", type())
                 .add("subject", subject())
-                .add("prevSubject", prevSubject())
+                .add("prevSubject", prevSubject)
+                .add("alternatives", alternativeRoutes)
                 .toString();
     }
 }
diff --git a/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/DefaultResolvedRouteStore.java b/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/DefaultResolvedRouteStore.java
index d3f97b9..fa07d14 100644
--- a/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/DefaultResolvedRouteStore.java
+++ b/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/DefaultResolvedRouteStore.java
@@ -16,6 +16,8 @@
 
 package org.onosproject.incubator.net.routing.impl;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.googlecode.concurrenttrees.common.KeyValuePair;
 import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
 import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
@@ -57,8 +59,8 @@
     }
 
     @Override
-    public RouteEvent updateRoute(ResolvedRoute route) {
-        return getDefaultRouteTable(route).update(route);
+    public RouteEvent updateRoute(ResolvedRoute route, Set<ResolvedRoute> alternatives) {
+        return getDefaultRouteTable(route).update(route, alternatives);
     }
 
     @Override
@@ -87,6 +89,11 @@
     }
 
     @Override
+    public Collection<ResolvedRoute> getAllRoutes(IpPrefix prefix) {
+        return getDefaultRouteTable(prefix.address()).getAllRoutes(prefix);
+    }
+
+    @Override
     public Optional<ResolvedRoute> longestPrefixMatch(IpAddress ip) {
         return getDefaultRouteTable(ip).longestPrefixMatch(ip);
     }
@@ -105,6 +112,7 @@
      */
     private class RouteTable {
         private final InvertedRadixTree<ResolvedRoute> routeTable;
+        private final Map<IpPrefix, Set<ResolvedRoute>> alternativeRoutes;
 
         /**
          * Creates a new route table.
@@ -112,27 +120,58 @@
         public RouteTable() {
             routeTable = new ConcurrentInvertedRadixTree<>(
                     new DefaultByteArrayNodeFactory());
+
+            alternativeRoutes = Maps.newHashMap();
         }
 
         /**
          * Adds or updates the route in the route table.
          *
          * @param route route to update
+         * @param alternatives alternative routes
          */
-        public RouteEvent update(ResolvedRoute route) {
+        public RouteEvent update(ResolvedRoute route, Set<ResolvedRoute> alternatives) {
+            Set<ResolvedRoute> immutableAlternatives = checkAlternatives(route, alternatives);
+
             synchronized (this) {
                 ResolvedRoute oldRoute = routeTable.put(createBinaryString(route.prefix()), route);
+                Set<ResolvedRoute> oldRoutes = alternativeRoutes.put(route.prefix(), immutableAlternatives);
 
-                // No need to proceed if the new route is the same
-                if (route.equals(oldRoute)) {
-                    return null;
+                if (!route.equals(oldRoute)) {
+                    if (oldRoute == null) {
+                        return new RouteEvent(RouteEvent.Type.ROUTE_ADDED, route,
+                                immutableAlternatives);
+                    } else {
+                        return new RouteEvent(RouteEvent.Type.ROUTE_UPDATED, route,
+                                oldRoute, immutableAlternatives);
+                    }
                 }
 
-                if (oldRoute == null) {
-                    return new RouteEvent(RouteEvent.Type.ROUTE_ADDED, route);
-                } else {
-                    return new RouteEvent(RouteEvent.Type.ROUTE_UPDATED, route, oldRoute);
+                if (!immutableAlternatives.equals(oldRoutes)) {
+                    return new RouteEvent(RouteEvent.Type.ALTERNATIVE_ROUTES_CHANGED,
+                            route, immutableAlternatives);
                 }
+
+                return null;
+            }
+        }
+
+        /**
+         * Checks that the best route is present in the alternatives list and
+         * returns an immutable set of alternatives.
+         *
+         * @param route best route
+         * @param alternatives alternatives
+         * @return immutable set of alternative routes
+         */
+        private Set<ResolvedRoute> checkAlternatives(ResolvedRoute route, Set<ResolvedRoute> alternatives) {
+            if (!alternatives.contains(route)) {
+                return ImmutableSet.<ResolvedRoute>builder()
+                        .addAll(alternatives)
+                        .add(route)
+                        .build();
+            } else {
+                return ImmutableSet.copyOf(alternatives);
             }
         }
 
@@ -146,6 +185,7 @@
                 String key = createBinaryString(prefix);
 
                 ResolvedRoute route = routeTable.getValueForExactKey(key);
+                alternativeRoutes.remove(prefix);
 
                 if (route != null) {
                     routeTable.remove(key);
@@ -176,6 +216,10 @@
             return Optional.ofNullable(routeTable.getValueForExactKey(createBinaryString(prefix)));
         }
 
+        public Collection<ResolvedRoute> getAllRoutes(IpPrefix prefix) {
+            return alternativeRoutes.getOrDefault(prefix, Collections.emptySet());
+        }
+
         /**
          * Performs a longest prefix match with the given IP in the route table.
          *
diff --git a/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/ResolvedRouteStore.java b/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/ResolvedRouteStore.java
index 55ca3f9..2285572 100644
--- a/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/ResolvedRouteStore.java
+++ b/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/ResolvedRouteStore.java
@@ -35,9 +35,10 @@
      * Adds or updates the best route for the given prefix.
      *
      * @param route new best route for this prefix
+     * @param alternatives alternative resolved routes
      * @return event describing the change
      */
-    RouteEvent updateRoute(ResolvedRoute route);
+    RouteEvent updateRoute(ResolvedRoute route, Set<ResolvedRoute> alternatives);
 
     /**
      * Removes the best route for the given prefix.
@@ -71,6 +72,15 @@
     Optional<ResolvedRoute> getRoute(IpPrefix prefix);
 
     /**
+     * Returns all resolved routes stored for the given prefix, including the
+     * best selected route.
+     *
+     * @param prefix IP prefix to look up routes for
+     * @return all stored resolved routes for this prefix
+     */
+    Collection<ResolvedRoute> getAllRoutes(IpPrefix prefix);
+
+    /**
      * Performs a longest prefix match of the best routes on the given IP address.
      *
      * @param ip IP address
diff --git a/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/RouteManager.java b/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/RouteManager.java
index 5b637b8..539b2cb 100644
--- a/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/RouteManager.java
+++ b/incubator/net/src/main/java/org/onosproject/incubator/net/routing/impl/RouteManager.java
@@ -23,7 +23,7 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
 import org.onlab.packet.IpAddress;
-import org.onosproject.event.ListenerService;
+import org.onlab.packet.IpPrefix;
 import org.onosproject.incubator.net.routing.InternalRouteEvent;
 import org.onosproject.incubator.net.routing.NextHop;
 import org.onosproject.incubator.net.routing.ResolvedRoute;
@@ -68,8 +68,7 @@
  */
 @Service
 @Component
-public class RouteManager implements ListenerService<RouteEvent, RouteListener>,
-        RouteService, RouteAdminService {
+public class RouteManager implements RouteService, RouteAdminService {
 
     private final Logger log = LoggerFactory.getLogger(getClass());
 
@@ -129,7 +128,8 @@
             resolvedRouteStore.getRouteTables().stream()
                     .map(resolvedRouteStore::getRoutes)
                     .flatMap(Collection::stream)
-                    .map(route -> new RouteEvent(RouteEvent.Type.ROUTE_ADDED, route))
+                    .map(route -> new RouteEvent(RouteEvent.Type.ROUTE_ADDED, route,
+                            resolvedRouteStore.getAllRoutes(route.prefix())))
                     .forEach(l::post);
 
             listeners.put(listener, l);
@@ -259,25 +259,31 @@
     }
 
     private ResolvedRoute decide(ResolvedRoute route1, ResolvedRoute route2) {
-        return Comparator.<ResolvedRoute, IpAddress>comparing(route -> route.nextHop())
+        return Comparator.comparing(ResolvedRoute::nextHop)
                        .compare(route1, route2) <= 0 ? route1 : route2;
     }
 
-    private void store(ResolvedRoute route) {
-        post(resolvedRouteStore.updateRoute(route));
+    private void store(ResolvedRoute route, Set<ResolvedRoute> alternatives) {
+        post(resolvedRouteStore.updateRoute(route, alternatives));
+    }
+
+    private void remove(IpPrefix prefix) {
+        post(resolvedRouteStore.removeRoute(prefix));
     }
 
     private void resolve(RouteSet routes) {
-        Optional<ResolvedRoute> resolvedRoute =
-        routes.routes().stream()
-                    .map(this::resolve)
-                    .filter(Objects::nonNull)
+        Set<ResolvedRoute> resolvedRoutes = routes.routes().stream()
+                .map(this::resolve)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        Optional<ResolvedRoute> bestRoute = resolvedRoutes.stream()
                     .reduce(this::decide);
 
-        if (resolvedRoute.isPresent()) {
-            this.store(resolvedRoute.get());
+        if (bestRoute.isPresent()) {
+            store(bestRoute.get(), resolvedRoutes);
         } else {
-            post(resolvedRouteStore.removeRoute(routes.prefix()));
+            remove(routes.prefix());
         }
     }
 
diff --git a/incubator/net/src/test/java/org/onosproject/incubator/net/routing/impl/RouteManagerTest.java b/incubator/net/src/test/java/org/onosproject/incubator/net/routing/impl/RouteManagerTest.java
index aeb2ff1..370b83b 100644
--- a/incubator/net/src/test/java/org/onosproject/incubator/net/routing/impl/RouteManagerTest.java
+++ b/incubator/net/src/test/java/org/onosproject/incubator/net/routing/impl/RouteManagerTest.java
@@ -199,7 +199,7 @@
     private void verifyRouteAdd(Route route, ResolvedRoute resolvedRoute) {
         reset(routeListener);
 
-        routeListener.event(new RouteEvent(RouteEvent.Type.ROUTE_ADDED, resolvedRoute));
+        routeListener.event(event(RouteEvent.Type.ROUTE_ADDED, resolvedRoute));
 
         replay(routeListener);
 
@@ -253,7 +253,7 @@
         // First add the original route
         addRoute(original);
 
-        routeListener.event(new RouteEvent(RouteEvent.Type.ROUTE_UPDATED,
+        routeListener.event(event(RouteEvent.Type.ROUTE_UPDATED,
                 updatedResolvedRoute, resolvedRoute));
         expectLastCall().once();
 
@@ -329,7 +329,7 @@
 
         // Now when we send the event, we expect the FIB update to be sent
         reset(routeListener);
-        routeListener.event(new RouteEvent(RouteEvent.Type.ROUTE_ADDED,
+        routeListener.event(event(RouteEvent.Type.ROUTE_ADDED,
                 new ResolvedRoute(route, MAC1, CP1)));
         replay(routeListener);
 
@@ -349,6 +349,14 @@
         verify(routeListener);
     }
 
+    private static RouteEvent event(RouteEvent.Type type, ResolvedRoute subject) {
+        return event(type, subject, null);
+    }
+
+    private static RouteEvent event(RouteEvent.Type type, ResolvedRoute subject, ResolvedRoute prevSubject) {
+        return new RouteEvent(type, subject, prevSubject, Collections.singleton(subject));
+    }
+
     /**
      * Test host service that stores a reference to the host listener.
      */