Port the Router functionality from SDN-IP.

As part of this we added an onlab-thirdparty artifact which allows us to
bring in dependencies that aren't bundles.
diff --git a/apps/sdnip/pom.xml b/apps/sdnip/pom.xml
index c8db20d..390b429 100644
--- a/apps/sdnip/pom.xml
+++ b/apps/sdnip/pom.xml
@@ -36,6 +36,12 @@
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-thirdparty</artifactId>
+    </dependency>
+
   </dependencies>
 
 </project>
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteEntry.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteEntry.java
new file mode 100644
index 0000000..16e20d2
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteEntry.java
@@ -0,0 +1,100 @@
+package org.onlab.onos.sdnip;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Represents a route entry for an IP prefix.
+ */
+public class RouteEntry {
+    private final IpPrefix prefix;             // The IP prefix
+    private final IpAddress nextHop;           // Next-hop IP address
+
+    /**
+     * Class constructor.
+     *
+     * @param prefix the IP prefix of the route
+     * @param nextHop the next hop IP address for the route
+     */
+    public RouteEntry(IpPrefix prefix, IpAddress nextHop) {
+        this.prefix = checkNotNull(prefix);
+        this.nextHop = checkNotNull(nextHop);
+    }
+
+    /**
+     * Returns the IP prefix of the route.
+     *
+     * @return the IP prefix of the route
+     */
+    public IpPrefix prefix() {
+        return prefix;
+    }
+
+    /**
+     * Returns the next hop IP address for the route.
+     *
+     * @return the next hop IP address for the route
+     */
+    public IpAddress nextHop() {
+        return nextHop;
+    }
+
+    /**
+     * Creates the binary string representation of an IPv4 prefix.
+     * The string length is equal to the prefix length.
+     *
+     * @param ip4Prefix the IPv4 prefix to use
+     * @return the binary string representation
+     */
+    static String createBinaryString(IpPrefix ip4Prefix) {
+        if (ip4Prefix.prefixLength() == 0) {
+            return "";
+        }
+
+        StringBuilder result = new StringBuilder(ip4Prefix.prefixLength());
+        long value = ip4Prefix.toRealInt();
+        for (int i = 0; i < ip4Prefix.prefixLength(); i++) {
+            long mask = 1 << (IpAddress.MAX_INET_MASK - 1 - i);
+            result.append(((value & mask) == 0) ? "0" : "1");
+        }
+        return result.toString();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        //
+        // NOTE: Subclasses are considered as change of identity, hence
+        // equals() will return false if the class type doesn't match.
+        //
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        RouteEntry otherRoute = (RouteEntry) other;
+        return Objects.equals(this.prefix, otherRoute.prefix) &&
+            Objects.equals(this.nextHop, otherRoute.nextHop);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(prefix, nextHop);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+            .add("prefix", prefix)
+            .add("nextHop", nextHop)
+            .toString();
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteListener.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteListener.java
new file mode 100644
index 0000000..424e348
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteListener.java
@@ -0,0 +1,13 @@
+package org.onlab.onos.sdnip;
+
+/**
+ * An interface to receive route updates from route providers.
+ */
+public interface RouteListener {
+    /**
+     * Receives a route update from a route provider.
+     *
+     * @param routeUpdate the updated route information
+     */
+    public void update(RouteUpdate routeUpdate);
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteUpdate.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteUpdate.java
new file mode 100644
index 0000000..a134a7a
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteUpdate.java
@@ -0,0 +1,91 @@
+package org.onlab.onos.sdnip;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Represents a change in routing information.
+ */
+public class RouteUpdate {
+    private final Type type;                    // The route update type
+    private final RouteEntry routeEntry;        // The updated route entry
+
+    /**
+     * Specifies the type of a route update.
+     * <p/>
+     * Route updates can either provide updated information for a route, or
+     * withdraw a previously updated route.
+     */
+    public enum Type {
+        /**
+         * The update contains updated route information for a route.
+         */
+        UPDATE,
+        /**
+         * The update withdraws the route, meaning any previous information is
+         * no longer valid.
+         */
+        DELETE
+    }
+
+    /**
+     * Class constructor.
+     *
+     * @param type the type of the route update
+     * @param routeEntry the route entry with the update
+     */
+    public RouteUpdate(Type type, RouteEntry routeEntry) {
+        this.type = type;
+        this.routeEntry = checkNotNull(routeEntry);
+    }
+
+    /**
+     * Returns the type of the route update.
+     *
+     * @return the type of the update
+     */
+    public Type type() {
+        return type;
+    }
+
+    /**
+     * Returns the route entry the route update is for.
+     *
+     * @return the route entry the route update is for
+     */
+    public RouteEntry routeEntry() {
+        return routeEntry;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        }
+
+        if (!(other instanceof RouteUpdate)) {
+            return false;
+        }
+
+        RouteUpdate otherUpdate = (RouteUpdate) other;
+
+        return Objects.equals(this.type, otherUpdate.type) &&
+            Objects.equals(this.routeEntry, otherUpdate.routeEntry);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, routeEntry);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+            .add("type", type)
+            .add("routeEntry", routeEntry)
+            .toString();
+    }
+}
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
new file mode 100644
index 0000000..be54222
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
@@ -0,0 +1,767 @@
+package org.onlab.onos.sdnip;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Host;
+import org.onlab.onos.net.flow.DefaultTrafficSelector;
+import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.flow.criteria.Criteria.IPCriterion;
+import org.onlab.onos.net.flow.criteria.Criterion;
+import org.onlab.onos.net.flow.criteria.Criterion.Type;
+import org.onlab.onos.net.host.HostEvent;
+import org.onlab.onos.net.host.HostListener;
+import org.onlab.onos.net.host.HostService;
+import org.onlab.onos.net.intent.Intent;
+import org.onlab.onos.net.intent.IntentId;
+import org.onlab.onos.net.intent.IntentService;
+import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
+import org.onlab.onos.sdnip.config.BgpPeer;
+import org.onlab.onos.sdnip.config.Interface;
+import org.onlab.onos.sdnip.config.SdnIpConfigService;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.googlecode.concurrenttrees.common.KeyValuePair;
+import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
+import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
+import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
+
+/**
+ * This class processes BGP route update, translates each update into a intent
+ * and submits the intent.
+ *
+ * TODO: Make it thread-safe.
+ */
+public class Router implements RouteListener {
+
+    private static final Logger log = LoggerFactory.getLogger(Router.class);
+
+    // Store all route updates in a InvertedRadixTree.
+    // The key in this Tree is the binary sting of prefix of route.
+    // The Ip4Address is the next hop address of route, and is also the value
+    // of each entry.
+    private InvertedRadixTree<RouteEntry> bgpRoutes;
+
+    // Stores all incoming route updates in a queue.
+    private BlockingQueue<RouteUpdate> routeUpdates;
+
+    // The Ip4Address is the next hop address of each route update.
+    private SetMultimap<IpAddress, RouteEntry> routesWaitingOnArp;
+    private ConcurrentHashMap<IpPrefix, MultiPointToSinglePointIntent> pushedRouteIntents;
+
+    private IntentService intentService;
+    //private IProxyArpService proxyArp;
+    private HostService hostService;
+    private SdnIpConfigService configInfoService;
+    private InterfaceService interfaceService;
+
+    private ExecutorService bgpUpdatesExecutor;
+    private ExecutorService bgpIntentsSynchronizerExecutor;
+
+    // TODO temporary
+    private int intentId = Integer.MAX_VALUE / 2;
+
+    //
+    // State to deal with SDN-IP Leader election and pushing Intents
+    //
+    private Semaphore intentsSynchronizerSemaphore = new Semaphore(0);
+    private volatile boolean isElectedLeader = false;
+    private volatile boolean isActivatedLeader = false;
+
+    // For routes announced by local BGP deamon in SDN network,
+    // the next hop will be 0.0.0.0.
+    public static final IpAddress LOCAL_NEXT_HOP = IpAddress.valueOf("0.0.0.0");
+
+    /**
+     * Class constructor.
+     *
+     * @param intentService the intent service
+     * @param proxyArp the proxy ARP service
+     * @param configInfoService the configuration service
+     * @param interfaceService the interface service
+     */
+    public Router(IntentService intentService, HostService hostService,
+            SdnIpConfigService configInfoService, InterfaceService interfaceService) {
+
+        this.intentService = intentService;
+        this.hostService = hostService;
+        this.configInfoService = configInfoService;
+        this.interfaceService = interfaceService;
+
+        bgpRoutes = new ConcurrentInvertedRadixTree<>(
+                new DefaultByteArrayNodeFactory());
+        routeUpdates = new LinkedBlockingQueue<>();
+        routesWaitingOnArp = Multimaps.synchronizedSetMultimap(
+                HashMultimap.<IpAddress, RouteEntry>create());
+        pushedRouteIntents = new ConcurrentHashMap<>();
+
+        bgpUpdatesExecutor = Executors.newSingleThreadExecutor(
+                new ThreadFactoryBuilder().setNameFormat("bgp-updates-%d").build());
+        bgpIntentsSynchronizerExecutor = Executors.newSingleThreadExecutor(
+                new ThreadFactoryBuilder()
+                .setNameFormat("bgp-intents-synchronizer-%d").build());
+    }
+
+    /**
+     * Starts the Router.
+     */
+    public void start() {
+
+        bgpUpdatesExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                doUpdatesThread();
+            }
+        });
+
+        bgpIntentsSynchronizerExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                doIntentSynchronizationThread();
+            }
+        });
+    }
+
+    //@Override TODO hook this up to something
+    public void leaderChanged(boolean isLeader) {
+        log.debug("Leader changed: {}", isLeader);
+
+        if (!isLeader) {
+            this.isElectedLeader = false;
+            this.isActivatedLeader = false;
+            return;                     // Nothing to do
+        }
+        this.isActivatedLeader = false;
+        this.isElectedLeader = true;
+
+        //
+        // Tell the Intents Synchronizer thread to start the synchronization
+        //
+        intentsSynchronizerSemaphore.release();
+    }
+
+    @Override
+    public void update(RouteUpdate routeUpdate) {
+        log.debug("Received new route Update: {}", routeUpdate);
+
+        try {
+            routeUpdates.put(routeUpdate);
+        } catch (InterruptedException e) {
+            log.debug("Interrupted while putting on routeUpdates queue", e);
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    /**
+     * Thread for Intent Synchronization.
+     */
+    private void doIntentSynchronizationThread() {
+        boolean interrupted = false;
+        try {
+            while (!interrupted) {
+                try {
+                    intentsSynchronizerSemaphore.acquire();
+                    //
+                    // Drain all permits, because a single synchronization is
+                    // sufficient.
+                    //
+                    intentsSynchronizerSemaphore.drainPermits();
+                } catch (InterruptedException e) {
+                    log.debug("Interrupted while waiting to become " +
+                              "Intent Synchronization leader");
+                    interrupted = true;
+                    break;
+                }
+                syncIntents();
+            }
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Thread for handling route updates.
+     */
+    private void doUpdatesThread() {
+        boolean interrupted = false;
+        try {
+            while (!interrupted) {
+                try {
+                    RouteUpdate update = routeUpdates.take();
+                    switch (update.type()) {
+                    case UPDATE:
+                        processRouteAdd(update.routeEntry());
+                        break;
+                    case DELETE:
+                        processRouteDelete(update.routeEntry());
+                        break;
+                    default:
+                        log.error("Unknown update Type: {}", update.type());
+                        break;
+                    }
+                } catch (InterruptedException e) {
+                    log.debug("Interrupted while taking from updates queue", e);
+                    interrupted = true;
+                } catch (Exception e) {
+                    log.debug("exception", e);
+                }
+            }
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Performs Intents Synchronization between the internally stored Route
+     * Intents and the installed Route Intents.
+     */
+    private void syncIntents() {
+        synchronized (this) {
+            if (!isElectedLeader) {
+                return;         // Nothing to do: not the leader anymore
+            }
+            log.debug("Syncing SDN-IP Route Intents...");
+
+            Map<IpPrefix, MultiPointToSinglePointIntent> fetchedIntents =
+                new HashMap<>();
+
+            //
+            // Fetch all intents, and classify the Multi-Point-to-Point Intents
+            // based on the matching prefix.
+            //
+            for (Intent intent : intentService.getIntents()) {
+                //
+                // TODO: Ignore all intents that are not installed by
+                // the SDN-IP application.
+                //
+                if (!(intent instanceof MultiPointToSinglePointIntent)) {
+                    continue;
+                }
+                MultiPointToSinglePointIntent mp2pIntent =
+                    (MultiPointToSinglePointIntent) intent;
+                /*Match match = mp2pIntent.getMatch();
+                if (!(match instanceof PacketMatch)) {
+                    continue;
+                }
+                PacketMatch packetMatch = (PacketMatch) match;
+                Ip4Prefix prefix = packetMatch.getDstIpAddress();
+                if (prefix == null) {
+                    continue;
+                }
+                fetchedIntents.put(prefix, mp2pIntent);*/
+                for (Criterion criterion : mp2pIntent.selector().criteria()) {
+                    if (criterion.type() == Type.IPV4_DST) {
+                        IPCriterion ipCriterion = (IPCriterion) criterion;
+                        fetchedIntents.put(ipCriterion.ip(), mp2pIntent);
+                    }
+                }
+
+            }
+
+            //
+            // Compare for each prefix the local IN-MEMORY Intents with the
+            // FETCHED Intents:
+            //  - If the IN-MEMORY Intent is same as the FETCHED Intent, store
+            //    the FETCHED Intent in the local memory (i.e., override the
+            //    IN-MEMORY Intent) to preserve the original Intent ID
+            //  - if the IN-MEMORY Intent is not same as the FETCHED Intent,
+            //    delete the FETCHED Intent, and push/install the IN-MEMORY
+            //    Intent.
+            //  - If there is an IN-MEMORY Intent for a prefix, but no FETCHED
+            //    Intent for same prefix, then push/install the IN-MEMORY
+            //    Intent.
+            //  - If there is a FETCHED Intent for a prefix, but no IN-MEMORY
+            //    Intent for same prefix, then delete/withdraw the FETCHED
+            //    Intent.
+            //
+            Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
+                storeInMemoryIntents = new LinkedList<>();
+            Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
+                addIntents = new LinkedList<>();
+            Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
+                deleteIntents = new LinkedList<>();
+            for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry :
+                     pushedRouteIntents.entrySet()) {
+                IpPrefix prefix = entry.getKey();
+                MultiPointToSinglePointIntent inMemoryIntent =
+                    entry.getValue();
+                MultiPointToSinglePointIntent fetchedIntent =
+                    fetchedIntents.get(prefix);
+
+                if (fetchedIntent == null) {
+                    //
+                    // No FETCHED Intent for same prefix: push the IN-MEMORY
+                    // Intent.
+                    //
+                    addIntents.add(Pair.of(prefix, inMemoryIntent));
+                    continue;
+                }
+
+                //
+                // If IN-MEMORY Intent is same as the FETCHED Intent,
+                // store the FETCHED Intent in the local memory.
+                //
+                if (compareMultiPointToSinglePointIntents(inMemoryIntent,
+                                                          fetchedIntent)) {
+                    storeInMemoryIntents.add(Pair.of(prefix, fetchedIntent));
+                } else {
+                    //
+                    // The IN-MEMORY Intent is not same as the FETCHED Intent,
+                    // hence delete the FETCHED Intent, and install the
+                    // IN-MEMORY Intent.
+                    //
+                    deleteIntents.add(Pair.of(prefix, fetchedIntent));
+                    addIntents.add(Pair.of(prefix, inMemoryIntent));
+                }
+                fetchedIntents.remove(prefix);
+            }
+
+            //
+            // Any remaining FETCHED Intents have to be deleted/withdrawn
+            //
+            for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry :
+                     fetchedIntents.entrySet()) {
+                IpPrefix prefix = entry.getKey();
+                MultiPointToSinglePointIntent fetchedIntent = entry.getValue();
+                deleteIntents.add(Pair.of(prefix, fetchedIntent));
+            }
+
+            //
+            // Perform the actions:
+            // 1. Store in memory fetched intents that are same. Can be done
+            //    even if we are not the leader anymore
+            // 2. Delete intents: check if the leader before each operation
+            // 3. Add intents: check if the leader before each operation
+            //
+            for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
+                     storeInMemoryIntents) {
+                IpPrefix prefix = pair.getLeft();
+                MultiPointToSinglePointIntent intent = pair.getRight();
+                log.debug("Intent synchronization: updating in-memory " +
+                          "Intent for prefix: {}", prefix);
+                pushedRouteIntents.put(prefix, intent);
+            }
+            //
+            isActivatedLeader = true;           // Allow push of Intents
+            for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
+                     deleteIntents) {
+                IpPrefix prefix = pair.getLeft();
+                MultiPointToSinglePointIntent intent = pair.getRight();
+                if (!isElectedLeader) {
+                    isActivatedLeader = false;
+                    return;
+                }
+                log.debug("Intent synchronization: deleting Intent for " +
+                          "prefix: {}", prefix);
+                intentService.withdraw(intent);
+            }
+            //
+            for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
+                     addIntents) {
+                IpPrefix prefix = pair.getLeft();
+                MultiPointToSinglePointIntent intent = pair.getRight();
+                if (!isElectedLeader) {
+                    isActivatedLeader = false;
+                    return;
+                }
+                log.debug("Intent synchronization: adding Intent for " +
+                          "prefix: {}", prefix);
+                intentService.submit(intent);
+            }
+            if (!isElectedLeader) {
+                isActivatedLeader = false;
+            }
+            log.debug("Syncing SDN-IP routes completed.");
+        }
+    }
+
+    /**
+     * Compares two Multi-point to Single Point Intents whether they represent
+     * same logical intention.
+     *
+     * @param intent1 the first Intent to compare
+     * @param intent2 the second Intent to compare
+     * @return true if both Intents represent same logical intention, otherwise
+     * false
+     */
+    private boolean compareMultiPointToSinglePointIntents(
+                                MultiPointToSinglePointIntent intent1,
+                                MultiPointToSinglePointIntent intent2) {
+        /*Match match1 = intent1.getMatch();
+        Match match2 = intent2.getMatch();
+        Action action1 = intent1.getAction();
+        Action action2 = intent2.getAction();
+        Set<SwitchPort> ingressPorts1 = intent1.getIngressPorts();
+        Set<SwitchPort> ingressPorts2 = intent2.getIngressPorts();
+        SwitchPort egressPort1 = intent1.getEgressPort();
+        SwitchPort egressPort2 = intent2.getEgressPort();
+
+        return Objects.equal(match1, match2) &&
+            Objects.equal(action1, action2) &&
+            Objects.equal(egressPort1, egressPort2) &&
+            Objects.equal(ingressPorts1, ingressPorts2);*/
+        return Objects.equal(intent1.selector(), intent2.selector()) &&
+                Objects.equal(intent1.treatment(), intent2.treatment()) &&
+                Objects.equal(intent1.ingressPoints(), intent2.ingressPoints()) &&
+                Objects.equal(intent1.egressPoint(), intent2.egressPoint());
+    }
+
+    /**
+     * Processes adding a route entry.
+     * <p/>
+     * Put new route entry into InvertedRadixTree. If there was an existing
+     * nexthop for this prefix, but the next hop was different, then execute
+     * deleting old route entry. If the next hop is the SDN domain, we do not
+     * handle it at the moment. Otherwise, execute adding a route.
+     *
+     * @param routeEntry the route entry to add
+     */
+    protected void processRouteAdd(RouteEntry routeEntry) {
+        synchronized (this) {
+            log.debug("Processing route add: {}", routeEntry);
+
+            IpPrefix prefix = routeEntry.prefix();
+            IpAddress nextHop = null;
+            RouteEntry foundRouteEntry =
+                bgpRoutes.put(RouteEntry.createBinaryString(prefix),
+                              routeEntry);
+            if (foundRouteEntry != null) {
+                nextHop = foundRouteEntry.nextHop();
+            }
+
+            if (nextHop != null && !nextHop.equals(routeEntry.nextHop())) {
+                // There was an existing nexthop for this prefix. This update
+                // supersedes that, so we need to remove the old flows for this
+                // prefix from the switches
+                executeRouteDelete(routeEntry);
+            }
+            if (nextHop != null && nextHop.equals(routeEntry.nextHop())) {
+                return;
+            }
+
+            if (routeEntry.nextHop().equals(LOCAL_NEXT_HOP)) {
+                // Route originated by SDN domain
+                // We don't handle these at the moment
+                log.debug("Own route {} to {}",
+                          routeEntry.prefix(), routeEntry.nextHop());
+                return;
+            }
+
+            executeRouteAdd(routeEntry);
+        }
+    }
+
+    /**
+     * Executes adding a route entry.
+     * <p/>
+     * Find out the egress Interface and MAC address of next hop router for
+     * this route entry. If the MAC address can not be found in ARP cache,
+     * then this prefix will be put in routesWaitingOnArp queue. Otherwise,
+     * new route intent will be created and installed.
+     *
+     * @param routeEntry the route entry to add
+     */
+    private void executeRouteAdd(RouteEntry routeEntry) {
+        log.debug("Executing route add: {}", routeEntry);
+
+        // See if we know the MAC address of the next hop
+        //MacAddress nextHopMacAddress =
+            //proxyArp.getMacAddress(routeEntry.getNextHop());
+        MacAddress nextHopMacAddress = null;
+        Set<Host> hosts = hostService.getHostsByIp(
+                routeEntry.nextHop().toPrefix());
+        if (!hosts.isEmpty()) {
+            // TODO how to handle if multiple hosts are returned?
+            nextHopMacAddress = hosts.iterator().next().mac();
+        }
+
+        if (nextHopMacAddress == null) {
+            routesWaitingOnArp.put(routeEntry.nextHop(), routeEntry);
+            //proxyArp.sendArpRequest(routeEntry.getNextHop(), this, true);
+            // TODO maybe just do this for every prefix anyway
+            hostService.startMonitoringIp(routeEntry.nextHop());
+            return;
+        }
+
+        addRouteIntentToNextHop(routeEntry.prefix(),
+                                routeEntry.nextHop(),
+                                nextHopMacAddress);
+    }
+
+    /**
+     * Adds a route intent given a prefix and a next hop IP address. This
+     * method will find the egress interface for the intent.
+     *
+     * @param prefix IP prefix of the route to add
+     * @param nextHopIpAddress IP address of the next hop
+     * @param nextHopMacAddress MAC address of the next hop
+     */
+    private void addRouteIntentToNextHop(IpPrefix prefix,
+                                         IpAddress nextHopIpAddress,
+                                         MacAddress nextHopMacAddress) {
+
+        // Find the attachment point (egress interface) of the next hop
+        Interface egressInterface;
+        if (configInfoService.getBgpPeers().containsKey(nextHopIpAddress)) {
+            // Route to a peer
+            log.debug("Route to peer {}", nextHopIpAddress);
+            BgpPeer peer =
+                configInfoService.getBgpPeers().get(nextHopIpAddress);
+            egressInterface =
+                interfaceService.getInterface(peer.connectPoint());
+        } else {
+            // Route to non-peer
+            log.debug("Route to non-peer {}", nextHopIpAddress);
+            egressInterface =
+                interfaceService.getMatchingInterface(nextHopIpAddress);
+            if (egressInterface == null) {
+                log.warn("No outgoing interface found for {}",
+                         nextHopIpAddress);
+                return;
+            }
+        }
+
+        doAddRouteIntent(prefix, egressInterface, nextHopMacAddress);
+    }
+
+    /**
+     * Installs a route intent for a prefix.
+     * <p/>
+     * Intent will match dst IP prefix and rewrite dst MAC address at all other
+     * border switches, then forward packets according to dst MAC address.
+     *
+     * @param prefix IP prefix from route
+     * @param egressInterface egress Interface connected to next hop router
+     * @param nextHopMacAddress MAC address of next hop router
+     */
+    private void doAddRouteIntent(IpPrefix prefix, Interface egressInterface,
+            MacAddress nextHopMacAddress) {
+        log.debug("Adding intent for prefix {}, next hop mac {}",
+                  prefix, nextHopMacAddress);
+
+        MultiPointToSinglePointIntent pushedIntent =
+            pushedRouteIntents.get(prefix);
+
+        // Just for testing.
+        if (pushedIntent != null) {
+            log.error("There should not be a pushed intent: {}", pushedIntent);
+        }
+
+        ConnectPoint egressPort = egressInterface.connectPoint();
+
+        Set<ConnectPoint> ingressPorts = new HashSet<>();
+
+        for (Interface intf : interfaceService.getInterfaces()) {
+            if (!intf.equals(egressInterface)) {
+                ConnectPoint srcPort = intf.connectPoint();
+                ingressPorts.add(srcPort);
+            }
+        }
+
+        // Match the destination IP prefix at the first hop
+        //PacketMatchBuilder builder = new PacketMatchBuilder();
+        //builder.setEtherType(Ethernet.TYPE_IPV4).setDstIpNet(prefix);
+        //PacketMatch packetMatch = builder.build();
+        TrafficSelector selector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
+                .matchIPDst(prefix)
+                .build();
+
+        // Rewrite the destination MAC address
+        //ModifyDstMacAction modifyDstMacAction =
+                //new ModifyDstMacAction(nextHopMacAddress);
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .setEthDst(nextHopMacAddress)
+                .build();
+
+        MultiPointToSinglePointIntent intent =
+                new MultiPointToSinglePointIntent(nextIntentId(),
+                        selector, treatment, ingressPorts, egressPort);
+
+        if (isElectedLeader && isActivatedLeader) {
+            log.debug("Intent installation: adding Intent for prefix: {}",
+                      prefix);
+            intentService.submit(intent);
+        }
+
+        // Maintain the Intent
+        pushedRouteIntents.put(prefix, intent);
+    }
+
+    /**
+     * Executes deleting a route entry.
+     * <p/>
+     * Removes prefix from InvertedRadixTree, if success, then try to delete
+     * the relative intent.
+     *
+     * @param routeEntry the route entry to delete
+     */
+    protected void processRouteDelete(RouteEntry routeEntry) {
+        synchronized (this) {
+            log.debug("Processing route delete: {}", routeEntry);
+            IpPrefix prefix = routeEntry.prefix();
+
+            // TODO check the change of logic here - remove doesn't check that
+            // the route entry was what we expected (and we can't do this
+            // concurrently)
+
+            if (bgpRoutes.remove(RouteEntry.createBinaryString(prefix))) {
+                //
+                // Only delete flows if an entry was actually removed from the
+                // tree. If no entry was removed, the <prefix, nexthop> wasn't
+                // there so it's probably already been removed and we don't
+                // need to do anything.
+                //
+                executeRouteDelete(routeEntry);
+            }
+
+            routesWaitingOnArp.remove(routeEntry.nextHop(), routeEntry);
+            // TODO cancel the request in the ARP manager as well
+        }
+    }
+
+    /**
+     * Executed deleting a route entry.
+     *
+     * @param routeEntry the route entry to delete
+     */
+    private void executeRouteDelete(RouteEntry routeEntry) {
+        log.debug("Executing route delete: {}", routeEntry);
+
+        IpPrefix prefix = routeEntry.prefix();
+
+        MultiPointToSinglePointIntent intent =
+            pushedRouteIntents.remove(prefix);
+
+        if (intent == null) {
+            log.debug("There is no intent in pushedRouteIntents to delete " +
+                      "for prefix: {}", prefix);
+        } else {
+            if (isElectedLeader && isActivatedLeader) {
+                log.debug("Intent installation: deleting Intent for prefix: {}",
+                          prefix);
+                intentService.withdraw(intent);
+            }
+        }
+    }
+
+    /**
+     * This method handles the prefixes which are waiting for ARP replies for
+     * MAC addresses of next hops.
+     *
+     * @param ipAddress next hop router IP address, for which we sent ARP
+     * request out
+     * @param macAddress MAC address which is relative to the ipAddress
+     */
+    //@Override
+    // TODO change name
+    public void arpResponse(IpAddress ipAddress, MacAddress macAddress) {
+        log.debug("Received ARP response: {} => {}", ipAddress, macAddress);
+
+         // We synchronize on this to prevent changes to the InvertedRadixTree
+         // while we're pushing intent. If the InvertedRadixTree changes, the
+         // InvertedRadixTree and intent could get out of sync.
+        synchronized (this) {
+
+            Set<RouteEntry> routesToPush =
+                routesWaitingOnArp.removeAll(ipAddress);
+
+            for (RouteEntry routeEntry : routesToPush) {
+                // These will always be adds
+                IpPrefix prefix = routeEntry.prefix();
+                String binaryString = RouteEntry.createBinaryString(prefix);
+                RouteEntry foundRouteEntry =
+                    bgpRoutes.getValueForExactKey(binaryString);
+                if (foundRouteEntry != null &&
+                    foundRouteEntry.nextHop().equals(routeEntry.nextHop())) {
+                    log.debug("Pushing prefix {} next hop {}",
+                              routeEntry.prefix(), routeEntry.nextHop());
+                    // We only push prefix flows if the prefix is still in the
+                    // InvertedRadixTree and the next hop is the same as our
+                    // update.
+                    // The prefix could have been removed while we were waiting
+                    // for the ARP, or the next hop could have changed.
+                    addRouteIntentToNextHop(prefix, ipAddress, macAddress);
+                } else {
+                    log.debug("Received ARP response, but {}/{} is no longer in"
+                            + " InvertedRadixTree", routeEntry.prefix(),
+                            routeEntry.nextHop());
+                }
+            }
+        }
+    }
+
+    /**
+     * Gets the SDN-IP routes.
+     *
+     * @return the SDN-IP routes
+     */
+    public Collection<RouteEntry> getRoutes() {
+        Iterator<KeyValuePair<RouteEntry>> it =
+                bgpRoutes.getKeyValuePairsForKeysStartingWith("").iterator();
+
+        List<RouteEntry> routes = new LinkedList<>();
+
+        while (it.hasNext()) {
+            KeyValuePair<RouteEntry> entry = it.next();
+            routes.add(entry.getValue());
+        }
+
+        return routes;
+    }
+
+    /**
+     * Generates a new unique intent ID.
+     *
+     * @return the new intent ID.
+     */
+    private IntentId nextIntentId() {
+        return new IntentId(intentId++);
+    }
+
+    /**
+     * Listener for host events.
+     */
+    class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            if (event.type() == HostEvent.Type.HOST_ADDED ||
+                    event.type() == HostEvent.Type.HOST_UPDATED) {
+                Host host = event.subject();
+                for (IpPrefix ip : host.ipAddresses()) {
+                    arpResponse(ip.toIpAddress(), host.mac());
+                }
+            }
+        }
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
index 25b13f1..a98a84b 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
@@ -9,7 +9,10 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.onlab.onos.net.host.HostService;
 import org.onlab.onos.net.intent.IntentService;
+import org.onlab.onos.sdnip.RouteUpdate.Type;
 import org.onlab.onos.sdnip.config.SdnIpConfigReader;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
 import org.slf4j.Logger;
 
 /**
@@ -28,6 +31,7 @@
 
     private SdnIpConfigReader config;
     private PeerConnectivity peerConnectivity;
+    private Router router;
 
     @Activate
     protected void activate() {
@@ -41,6 +45,14 @@
         peerConnectivity = new PeerConnectivity(config, interfaceService, intentService);
         peerConnectivity.start();
 
+        router = new Router(intentService, hostService, config, interfaceService);
+        router.start();
+
+        // TODO need to disable link discovery on external ports
+
+        router.update(new RouteUpdate(Type.UPDATE, new RouteEntry(
+                IpPrefix.valueOf("172.16.20.0/24"),
+                IpAddress.valueOf("192.168.10.1"))));
     }
 
     @Deactivate
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouteEntryTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouteEntryTest.java
new file mode 100644
index 0000000..45371f7
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouteEntryTest.java
@@ -0,0 +1,143 @@
+package org.onlab.onos.sdnip;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+
+/**
+ * Unit tests for the RouteEntry class.
+ */
+public class RouteEntryTest {
+    /**
+     * Tests valid class constructor.
+     */
+    @Test
+    public void testConstructor() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+
+        RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
+        assertThat(routeEntry.toString(),
+                   is("RouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8}"));
+    }
+
+    /**
+     * Tests invalid class constructor for null IPv4 prefix.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullPrefix() {
+        IpPrefix prefix = null;
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+
+        new RouteEntry(prefix, nextHop);
+    }
+
+    /**
+     * Tests invalid class constructor for null IPv4 next-hop.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullNextHop() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = null;
+
+        new RouteEntry(prefix, nextHop);
+    }
+
+    /**
+     * Tests getting the fields of a route entry.
+     */
+    @Test
+    public void testGetFields() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+
+        RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
+        assertThat(routeEntry.prefix(), is(prefix));
+        assertThat(routeEntry.nextHop(), is(nextHop));
+    }
+
+    /**
+     * Tests creating a binary string from IPv4 prefix.
+     */
+    @Test
+    public void testCreateBinaryString() {
+        IpPrefix prefix;
+
+        prefix = IpPrefix.valueOf("0.0.0.0/0");
+        assertThat(RouteEntry.createBinaryString(prefix), is(""));
+
+        prefix = IpPrefix.valueOf("192.168.166.0/22");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("1100000010101000101001"));
+
+        prefix = IpPrefix.valueOf("192.168.166.0/23");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("11000000101010001010011"));
+
+        prefix = IpPrefix.valueOf("192.168.166.0/24");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("110000001010100010100110"));
+
+        prefix = IpPrefix.valueOf("130.162.10.1/25");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("1000001010100010000010100"));
+
+        prefix = IpPrefix.valueOf("255.255.255.255/32");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("11111111111111111111111111111111"));
+    }
+
+    /**
+     * Tests equality of {@link RouteEntry}.
+     */
+    @Test
+    public void testEquality() {
+        IpPrefix prefix1 = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop1 = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry1 = new RouteEntry(prefix1, nextHop1);
+
+        IpPrefix prefix2 = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop2 = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry2 = new RouteEntry(prefix2, nextHop2);
+
+        assertThat(routeEntry1, is(routeEntry2));
+    }
+
+    /**
+     * Tests non-equality of {@link RouteEntry}.
+     */
+    @Test
+    public void testNonEquality() {
+        IpPrefix prefix1 = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop1 = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry1 = new RouteEntry(prefix1, nextHop1);
+
+        IpPrefix prefix2 = IpPrefix.valueOf("1.2.3.0/25");        // Different
+        IpAddress nextHop2 = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry2 = new RouteEntry(prefix2, nextHop2);
+
+        IpPrefix prefix3 = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop3 = IpAddress.valueOf("5.6.7.9");        // Different
+        RouteEntry routeEntry3 = new RouteEntry(prefix3, nextHop3);
+
+        assertThat(routeEntry1, is(not(routeEntry2)));
+        assertThat(routeEntry1, is(not(routeEntry3)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
+
+        assertThat(routeEntry.toString(),
+                   is("RouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8}"));
+    }
+}
diff --git a/features/features.xml b/features/features.xml
index 34c70bc..f0430e4 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -30,6 +30,7 @@
 
 	<bundle>mvn:org.codehaus.jackson/jackson-core-asl/1.9.13</bundle>
 	<bundle>mvn:org.codehaus.jackson/jackson-mapper-asl/1.9.13</bundle>
+	<bundle>mvn:org.onlab.onos/onlab-thirdparty/1.0.0-SNAPSHOT</bundle>
     </feature>
 
     <feature name="onos-thirdparty-web" version="1.0.0"
diff --git a/pom.xml b/pom.xml
index 08def13..daeb33e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,7 +107,13 @@
             </dependency>
 
             <dependency>
-                <groupId>commons-lang</groupId>
+              <groupId>com.googlecode.concurrent-trees</groupId>
+              <artifactId>concurrent-trees</artifactId>
+              <version>2.4.0</version>
+            </dependency>
+
+            <dependency>
+              <groupId>commons-lang</groupId>
                 <artifactId>commons-lang</artifactId>
                 <version>2.6</version>
             </dependency>
@@ -266,6 +272,13 @@
                 <artifactId>onos-of-api</artifactId>
                 <version>${project.version}</version>
             </dependency>
+
+            <dependency>
+              <groupId>org.onlab.onos</groupId>
+              <artifactId>onlab-thirdparty</artifactId>
+	      <version>${project.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>org.onlab.onos</groupId>
                 <artifactId>onos-of-api</artifactId>
diff --git a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
index b09430d..440256b 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
@@ -191,6 +191,15 @@
     }
 
     /**
+     * Converts the IP address to a /32 IP prefix.
+     *
+     * @return the new IP prefix
+     */
+    public IpPrefix toPrefix() {
+        return IpPrefix.valueOf(octets, MAX_INET_MASK);
+    }
+
+    /**
      * Helper for computing the mask value from CIDR.
      *
      * @return an integer bitmask
diff --git a/utils/thirdparty/pom.xml b/utils/thirdparty/pom.xml
new file mode 100644
index 0000000..59ab818
--- /dev/null
+++ b/utils/thirdparty/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.onlab.onos</groupId>
+    <artifactId>onlab-utils</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>onlab-thirdparty</artifactId>
+  <packaging>bundle</packaging>
+
+  <description>ONLab third-party dependencies</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.googlecode.concurrent-trees</groupId>
+      <artifactId>concurrent-trees</artifactId>
+      <version>2.4.0</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>2.3</version>
+        <configuration>
+          <filters>
+            <filter>
+              <artifact>com.googlecode.concurrent-trees:concurrent-trees</artifact>
+              <includes>
+                <include>com/googlecode/**</include>
+              </includes>
+              
+            </filter>
+            <filter>
+              <artifact>com.google.guava:guava</artifact>
+              <excludes>
+                <exclude>**</exclude>
+              </excludes>
+            </filter>
+          </filters>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>            
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <configuration>
+          <instructions>
+            <Export-Package>
+              com.googlecode.concurrenttrees.*
+            </Export-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java b/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java
new file mode 100644
index 0000000..df7c48a
--- /dev/null
+++ b/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java
@@ -0,0 +1,11 @@
+package org.onlab.thirdparty;
+
+
+/**
+ * Empty class required to get the onlab-thirdparty module to build properly.
+ * <p/>
+ * TODO Figure out how to remove this.
+ */
+public class OnlabThirdparty {
+
+}