org.limewire.mojito.routing.impl.RouteTableImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.limewire.mojito.routing.impl.RouteTableImpl.java

Source

/*
 * Mojito Distributed Hash Table (Mojito DHT)
 * Copyright (C) 2006-2007 LimeWire LLC
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.limewire.mojito.routing.impl;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.collection.PatriciaTrie;
import org.limewire.collection.Trie.Cursor;
import org.limewire.mojito.KUID;
import org.limewire.mojito.concurrent.DHTExecutorService;
import org.limewire.mojito.concurrent.DHTFutureAdapter;
import org.limewire.mojito.concurrent.DHTFutureListener;
import org.limewire.mojito.exceptions.DHTTimeoutException;
import org.limewire.mojito.result.PingResult;
import org.limewire.mojito.routing.Bucket;
import org.limewire.mojito.routing.ClassfulNetworkCounter;
import org.limewire.mojito.routing.Contact;
import org.limewire.mojito.routing.ContactFactory;
import org.limewire.mojito.routing.RouteTable;
import org.limewire.mojito.routing.Vendor;
import org.limewire.mojito.routing.Version;
import org.limewire.mojito.routing.RouteTable.RouteTableEvent.EventType;
import org.limewire.mojito.settings.RouteTableSettings;
import org.limewire.mojito.util.ContactUtils;
import org.limewire.mojito.util.ExceptionUtils;
import org.limewire.service.ErrorService;

/**
 * A PatriciaTrie based RouteTable implementation for the Mojito DHT.
 * This is the reference implementation.
 */
public class RouteTableImpl implements RouteTable {

    private static final long serialVersionUID = -7351267868357880369L;

    private static final Log LOG = LogFactory.getLog(RouteTableImpl.class);

    /**
     * Trie of Buckets and the Buckets are a Trie of Contacts.
     */
    private final PatriciaTrie<KUID, Bucket> bucketTrie;

    /**
     * A counter for consecutive failures.
     */
    private int consecutiveFailures = 0;

    /**
     * A reference to the ContactPinger.
     */
    private transient ContactPinger pinger;

    /**
     * The local Node.
     */
    private Contact localNode;

    /**
     * A list of RouteTableListeners.
     */
    private transient volatile List<RouteTableListener> listeners = new CopyOnWriteArrayList<RouteTableListener>();

    /** 
     * Executor where to offload notifications to RouteTableListeners.
     */
    private transient volatile DHTExecutorService notifier;

    /**
     * Create a new RouteTable and generates a new random Node ID
     * for the local Node.
     */
    public RouteTableImpl() {
        this(KUID.createRandomID());
    }

    /**
     * Create a new RouteTable and uses the given Node ID
     * for the local Node.
     */
    public RouteTableImpl(byte[] nodeId) {
        this(KUID.createWithBytes(nodeId));
    }

    /**
     * Create a new RouteTable and uses the given Node ID
     * for the local Node.
     */
    public RouteTableImpl(String nodeId) {
        this(KUID.createWithHexString(nodeId));
    }

    /**
     * Create a new RouteTable and uses the given Node ID
     * for the local Node.
     */
    public RouteTableImpl(KUID nodeId) {
        localNode = ContactFactory.createLocalContact(Vendor.UNKNOWN, Version.ZERO, nodeId, 0, false);
        bucketTrie = new PatriciaTrie<KUID, Bucket>(KUID.KEY_ANALYZER);
        init();
    }

    /**
     * Initializes the RouteTable.
     */
    private void init() {
        KUID bucketId = KUID.MINIMUM;
        Bucket bucket = new BucketNode(this, bucketId, 0);
        bucketTrie.put(bucketId, bucket);

        addContactToBucket(bucket, localNode);

        consecutiveFailures = 0;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        listeners = new CopyOnWriteArrayList<RouteTableListener>();

        // Post-Init the Buckets
        for (Bucket bucket : bucketTrie.values()) {
            ((BucketNode) bucket).postInit();
        }
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#setContactPinger(com.limegroup.mojito.routing.RouteTable.ContactPinger)
     */
    public void setContactPinger(ContactPinger pinger) {
        this.pinger = pinger;
    }

    public void setNotifier(DHTExecutorService executor) {
        this.notifier = executor;
    }

    /**
     * Adds a RouteTableListener.
     * <p>
     * Implementation Note: The listener(s) is not called from a 
     * separate event Thread! That means processor intensive tasks
     * that are performed straight in the listener(s) can slowdown 
     * the processing throughput significantly. Offload intensive
     * tasks to separate Threads in necessary!
     * 
     * @param l the RouteTableListener instance to add
     */
    public void addRouteTableListener(RouteTableListener l) {
        if (l == null) {
            throw new NullPointerException("RouteTableListener is null");
        }

        listeners.add(l);
    }

    /**
     * Removes a RouteTableListener.
     * 
     * @param l the RouteTableListener instance to remove
     */
    public void removeRouteTableListener(RouteTableListener l) {
        if (l == null) {
            throw new NullPointerException("RouteTableListener is null");
        }

        listeners.remove(l);
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#add(com.limegroup.mojito.Contact)
     */
    public synchronized void add(Contact node) {

        if (localNode.equals(node)) {
            String msg = "Cannot add the local Node: " + node;

            if (LOG.isErrorEnabled()) {
                LOG.error(msg);
            }

            ErrorService.error(new IllegalArgumentException(msg));
            return;
        }

        // Don't add firewalled Nodes
        if (node.isFirewalled()) {
            if (LOG.isTraceEnabled()) {
                LOG.trace(node + " is firewalled");
            }
            return;
        }

        // Make sure we're not mixing IPv4 and IPv6 addresses in the
        // RouteTable! IPv6 to IPv4 might work if there's a 6to4 gateway
        // or whatsoever but the opposite direction doesn't. An immediate
        // idea is to mark IPv6 Nodes as firewalled if they're contacting
        // IPv4 Nodes but this would lead to problems in the IPv6 network
        // if some IPv6 Nodes don't have access to a 6to4 gateway...
        if (!ContactUtils.isSameAddressSpace(localNode, node)) {

            // Log as ERROR so that we're not missing this
            if (LOG.isErrorEnabled()) {
                LOG.error(node + " is from a different IP address space than " + localNode);
            }
            return;
        }

        consecutiveFailures = 0;

        KUID nodeId = node.getNodeID();
        Bucket bucket = bucketTrie.select(nodeId);
        Contact existing = bucket.get(nodeId);

        if (existing != null) {
            updateContactInBucket(bucket, existing, node);
        } else if (!bucket.isActiveFull()) {
            if (isOkayToAdd(bucket, node)) {
                addContactToBucket(bucket, node);
            } else {
                // only cache node if the bucket can't be split
                if (!canSplit(bucket)) {
                    addContactToBucketCache(bucket, node);
                }
            }
        } else if (split(bucket)) {
            add(node); // re-try to add
        } else {
            replaceContactInBucket(bucket, node);
        }
    }

    /**
     * This method updates an existing Contact with data from a new Contact.
     * The initial state is that both Contacts have the same Node ID which
     * doesn't mean they're really the same Node. In order to figure out
     * if they're really equal it's performing some additional checks and
     * there are a few side conditions.
     */
    protected synchronized void updateContactInBucket(Bucket bucket, Contact existing, Contact node) {
        assert (existing.getNodeID().equals(node.getNodeID()));

        if (isLocalNode(existing)) {
            // The other Node collides with our Node ID! Do nothing,
            // the other guy will change its Node ID! If it doesn't
            // everybody who has us in their RouteTable will ping us
            // to check if we're alive and we're hopefully able to
            // respond. Besides that there isn't much we can do. :-/
            if (!isLocalNode(node)) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug(node + " collides with " + existing);
                }

                // Must be instance of LocalContact!
            } else if (!(node instanceof LocalContact)) {
                String msg = "Attempting to replace the local Node " + existing + " with " + node;

                if (LOG.isErrorEnabled()) {
                    LOG.error(msg);
                }

                throw new IllegalArgumentException(msg);

                // Alright, replace the existing Contact with the new
                // LocalContact. Log a warning... 
            } else {
                if (LOG.isWarnEnabled()) {
                    LOG.warn("Updating " + existing + " with " + node);
                }

                bucket.updateContact(node);
                this.localNode = node;

                fireContactUpdate(bucket, existing, node);
            }

            return;
        }

        /*
         * A non-live Contact will never replace a live Contact!
         */
        if (existing.isAlive() && !node.isAlive()) {
            return;
        }

        if (!existing.isAlive() || isLocalNode(node) || existing.equals(node) // <- checks only nodeId + address!
                || ContactUtils.areLocalContacts(existing, node)) {

            /*
             * See JIRA issue MOJITO-54
             */

            node.updateWithExistingContact(existing);
            Contact replaced = bucket.updateContact(node);
            assert (replaced == existing);

            // a good time to ping least recently seen node if we know we
            // have a node alive in the replacement cache. Don't do this too often!
            long delay = System.currentTimeMillis() - bucket.getTimeStamp();
            if (bucket.containsCachedContact(node.getNodeID())
                    && (delay > RouteTableSettings.BUCKET_PING_LIMIT.getValue())) {
                pingLeastRecentlySeenNode(bucket);
            }
            touchBucket(bucket);

            fireContactUpdate(bucket, existing, node);

        } else if (node.isAlive() && !existing.hasBeenRecentlyAlive()) {

            doSpoofCheck(bucket, existing, node);
        }
    }

    /**
     * This method tries to ping the existing Contact and if it doesn't
     * respond it will try to replace it with the new Contact. The initial 
     * state is that both Contacts have the same Node ID.
     */
    protected synchronized void doSpoofCheck(Bucket bucket, final Contact existing, final Contact node) {
        DHTFutureListener<PingResult> listener = new DHTFutureAdapter<PingResult>() {
            @Override
            public void handleFutureSuccess(PingResult result) {
                if (LOG.isWarnEnabled()) {
                    LOG.warn(node + " is trying to spoof " + result);
                }

                // DO NOTHING! The DefaultMessageHandler takes care 
                // of everything else! DO NOT BAN THE NODE!!!
                // Reason: It was maybe just a Node ID collision!
            }

            @Override
            public void handleExecutionException(ExecutionException e) {
                DHTTimeoutException timeout = ExceptionUtils.getCause(e, DHTTimeoutException.class);

                // We can only make decisions for timeouts! 
                if (timeout == null) {
                    return;
                }

                KUID nodeId = timeout.getNodeID();
                SocketAddress address = timeout.getSocketAddress();

                if (LOG.isInfoEnabled()) {
                    LOG.info(
                            ContactUtils.toString(nodeId, address) + " did not respond! Replacing it with " + node);
                }

                synchronized (RouteTableImpl.this) {
                    Bucket bucket = bucketTrie.select(nodeId);
                    Contact current = bucket.get(nodeId);
                    if (current != null && current.equals(existing)) {

                        /*
                         * See JIRA issue MOJITO-54
                         */

                        // NOTE: We cannot call updateContactInBucket(...) here
                        // because it would do the spoof check again.
                        node.updateWithExistingContact(current);
                        Contact replaced = bucket.updateContact(node);
                        assert (replaced == current);

                        fireContactUpdate(bucket, current, node);

                        // If the Node is in the Cache then ping the least recently
                        // seen live Node which might promote the new Node to a
                        // live Contact!
                        if (bucket.containsCachedContact(nodeId)) {
                            pingLeastRecentlySeenNode(bucket);
                        }
                    } else {
                        add(node);
                    }
                }
            }
        };

        fireContactCheck(bucket, existing, node);

        ping(existing, listener);
        touchBucket(bucket);
    }

    /**
     * This method adds the given Contact to the given Bucket.
     */
    protected synchronized void addContactToBucket(Bucket bucket, Contact node) {
        bucket.addActiveContact(node);
        fireActiveContactAdded(bucket, node);
    }

    /**
     * Adds the given Contact to the Bucket's replacement Cache.
     */
    protected synchronized void addContactToBucketCache(Bucket bucket, Contact node) {

        if (LOG.isTraceEnabled()) {
            LOG.trace("Adding " + node + " to " + bucket + " replacement cache");
        }

        // If the cache is full the least recently seen
        // node will be evicted!
        Contact existing = bucket.addCachedContact(node);
        fireCachedContactAdded(bucket, existing, node);
    }

    /**
     * Returns true if the bucket can be split.
     */
    private boolean canSplit(Bucket bucket) {
        // Three conditions for splitting:
        // 1. Bucket contains the local Node
        // 2. New node part of the smallest subtree to the local node
        // 3. current_depth mod symbol_size != 0

        boolean containsLocalNode = bucket.contains(getLocalNode().getNodeID());

        if (containsLocalNode || bucket.isInSmallestSubtree() || !bucket.isTooDeep()) {
            return true;
        }
        return false;
    }

    /**
     * This method splits the given Bucket into two new Buckets.
     * There are a few conditions in which cases we do split and
     * in which cases we don't.
     */
    protected synchronized boolean split(Bucket bucket) {

        if (canSplit(bucket)) {

            if (LOG.isTraceEnabled()) {
                LOG.trace("Splitting bucket: " + bucket);
            }

            List<Bucket> buckets = bucket.split();
            assert (buckets.size() == 2);

            Bucket left = buckets.get(0);
            Bucket right = buckets.get(1);

            // The left one replaces the current bucket in the Trie!
            Bucket oldLeft = bucketTrie.put(left.getBucketID(), left);
            assert (oldLeft == bucket);

            // The right one is new in the Trie!
            Bucket oldRight = bucketTrie.put(right.getBucketID(), right);
            assert (oldRight == null);

            fireSplitBucket(bucket, left, right);

            // WHOHOOO! WE SPLIT THE BUCKET!!!
            return true;
        }

        return false;
    }

    /**
     * This method tries to replace an existing Contact in the given
     * Bucket with the given Contact or tries to add the given Contact
     * to the Bucket's replacement Cache. There are certain conditions
     * in which cases we replace Contacts and if it's not possible we're
     * trying to add the Contact to the replacement cache.
     */
    protected synchronized void replaceContactInBucket(Bucket bucket, Contact node) {

        if (node.isAlive() && isOkayToAdd(bucket, node)) {
            Contact leastRecentlySeen = bucket.getLeastRecentlySeenActiveContact();

            // If all Contacts in the given Bucket have the same time
            // stamp as the local Node then it's possible that the lrs
            // Contact is the local Contact in which case we don't want 
            // to replace the local Contact with the given Contact

            // Is the least recently seen node in UNKNOWN or DEAD state OR is the 
            // new Node a priority Node AND the lrs Node is NOT the local Node

            if (!isLocalNode(leastRecentlySeen) && (leastRecentlySeen.isUnknown() || leastRecentlySeen.isDead()
                    || (node.getTimeStamp() == Contact.PRIORITY_CONTACT))) {

                if (LOG.isTraceEnabled()) {
                    LOG.info("Replacing " + leastRecentlySeen + " with " + node);
                }

                boolean removed = bucket.removeActiveContact(leastRecentlySeen.getNodeID());
                assert (removed == true);

                bucket.addActiveContact(node);
                touchBucket(bucket);

                fireReplaceContact(bucket, leastRecentlySeen, node);

                return;
            }
        }

        addContactToBucketCache(bucket, node);
        pingLeastRecentlySeenNode(bucket);
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#handleFailure(com.limegroup.mojito.KUID, java.net.SocketAddress)
     */
    public synchronized void handleFailure(KUID nodeId, SocketAddress address) {

        // NodeID might be null if we sent a ping to
        // an unknown Node (i.e. we knew only the
        // address) and the ping failed. 
        if (nodeId == null) {
            return;
        }

        // This should never happen -- who knows?!!
        if (nodeId.equals(getLocalNode().getNodeID())) {
            if (LOG.isErrorEnabled()) {
                LOG.error("Cannot handle local Node's errors: " + ContactUtils.toString(nodeId, address));
            }
            return;
        }

        Bucket bucket = bucketTrie.select(nodeId);
        Contact node = bucket.get(nodeId);
        if (node == null) {
            // It's neither a live nor a cached Node
            // in the bucket!
            return;
        }

        if (!node.getContactAddress().equals(address)) {
            if (LOG.isWarnEnabled()) {
                LOG.warn(node + " address and " + address + " do not match");
            }
            return;
        }

        // Ignore failure if we start getting to many disconnections in a row
        if (consecutiveFailures >= RouteTableSettings.MAX_CONSECUTIVE_FAILURES.getValue()) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Ignoring node failure as it appears that we are disconnected");
            }
            return;
        }
        consecutiveFailures++;

        node.handleFailure();
        if (node.isDead()) {

            if (bucket.containsActiveContact(nodeId)) {
                if (LOG.isTraceEnabled()) {
                    LOG.trace("Removing " + node + " and replacing it with the MRS Node from Cache");
                }

                // Remove a live-dead Contact only if there's something 
                // in the replacement cache or if the Node has too many
                // errors

                if (bucket.getCacheSize() > 0) {

                    Contact mrs = null;
                    while ((mrs = bucket.getMostRecentlySeenCachedContact()) != null) {
                        boolean removed = bucket.removeCachedContact(mrs.getNodeID());
                        assert (removed == true);

                        if (isOkayToAdd(bucket, mrs)) {
                            removed = bucket.removeActiveContact(nodeId);
                            assert (removed == true);
                            assert (bucket.isActiveFull() == false);

                            bucket.addActiveContact(mrs);
                            fireReplaceContact(bucket, node, mrs);
                            break;
                        }
                    }

                } else if (node.getFailures() >= RouteTableSettings.MAX_ACCEPT_NODE_FAILURES.getValue()) {

                    bucket.removeActiveContact(nodeId);
                    assert (bucket.isActiveFull() == false);

                    fireRemoveContact(bucket, node);
                }
            } else {

                // On first glance this might look like as if it is
                // not necessary since we're never contacting cached
                // Contacts but that's not absolutely true. FIND_NODE
                // lookups may return Contacts that are in our cache
                // and if they don't respond we want to remove them...

                if (LOG.isTraceEnabled()) {
                    LOG.trace("Removing " + node + " from Cache");
                }

                boolean removed = bucket.removeCachedContact(nodeId);
                assert (removed == true);
            }
        }
    }

    /**
     * Returns true of it's Okay to add the given Contact to the
     * given Bucket as active Contact. See {@link ClassfulNetworkCounter}
     * for more information!
     */
    protected synchronized boolean isOkayToAdd(Bucket bucket, Contact node) {
        ClassfulNetworkCounter counter = bucket.getClassfulNetworkCounter();
        boolean okay = (counter == null || counter.isOkayToAdd(node));

        if (LOG.isTraceEnabled()) {
            if (okay) {
                LOG.trace("It's okay to add " + node + " to " + bucket);
            } else {
                LOG.trace("It's NOT okay to add " + node + " to " + bucket);
            }
        }

        return okay;
    }

    /**
     * Removes the given Contact from the RouteTable.
     */
    protected synchronized boolean remove(Contact node) {
        return remove(node.getNodeID());
    }

    /**
     * Removes the given KUID (Contact with that KUID) 
     * from the RouteTable.
     */
    protected synchronized boolean remove(KUID nodeId) {
        return bucketTrie.select(nodeId).remove(nodeId);
    }

    /*
     * (non-Javadoc)
     * @see org.limewire.mojito.routing.RouteTable#getBucket(org.limewire.mojito.KUID)
     */
    public synchronized Bucket getBucket(KUID nodeId) {
        return bucketTrie.select(nodeId);
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#select(com.limegroup.mojito.KUID)
     */
    public synchronized Contact select(final KUID nodeId) {
        final Contact[] node = new Contact[] { null };
        bucketTrie.select(nodeId, new Cursor<KUID, Bucket>() {
            public SelectStatus select(Entry<? extends KUID, ? extends Bucket> entry) {
                node[0] = entry.getValue().select(nodeId);
                if (node[0] != null) {
                    return SelectStatus.EXIT;
                }
                return SelectStatus.CONTINUE;
            }
        });
        return node[0];
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#get(com.limegroup.mojito.KUID)
     */
    public synchronized Contact get(KUID nodeId) {
        return bucketTrie.select(nodeId).get(nodeId);
    }

    /**
     * Returns 'count' number of Contacts that are nearest (XOR distance)
     * to the given KUID.
     */
    public synchronized Collection<Contact> select(KUID nodeId, int count) {
        return select(nodeId, count, SelectMode.ALL);
    }

    /*
     * (non-Javadoc)
     * @see org.limewire.mojito.routing.RouteTable#select(org.limewire.mojito.KUID, int, org.limewire.mojito.routing.RouteTable.SelectMode)
     */
    public synchronized Collection<Contact> select(final KUID nodeId, final int count, final SelectMode mode) {

        if (count == 0) {
            return Collections.emptyList();
        }

        final int maxNodeFailures = RouteTableSettings.MAX_ACCEPT_NODE_FAILURES.getValue();
        final List<Contact> nodes = new ArrayList<Contact>(count);
        bucketTrie.select(nodeId, new Cursor<KUID, Bucket>() {
            public SelectStatus select(Entry<? extends KUID, ? extends Bucket> entry) {
                Bucket bucket = entry.getValue();

                Collection<Contact> list = null;
                if (mode == SelectMode.ALIVE || mode == SelectMode.ALIVE_WITH_LOCAL) {
                    // Select all Contacts from the Bucket to compensate
                    // the fact that not all of them will be alive. We're
                    // using Bucket.select() instead of Bucket.getActiveContacts()
                    // to get the Contacts sorted by xor distance!
                    list = bucket.select(nodeId, bucket.getActiveSize());
                } else {
                    list = bucket.select(nodeId, count);
                }

                for (Contact node : list) {

                    // Exit the loop if done
                    if (nodes.size() >= count) {
                        return SelectStatus.EXIT;
                    }

                    // Ignore all non-alive Contacts if only
                    // active Contacts are requested.
                    // We also ignore the local contact here (see LocalContact.isAlive)
                    // because a node will always have himself in the routing table
                    if (mode == SelectMode.ALIVE && !node.isAlive()) {
                        continue;
                    }

                    if (mode == SelectMode.ALIVE_WITH_LOCAL && !node.isAlive() && !isLocalNode(node)) {
                        continue;
                    }

                    // Ignore all Contacts that are down
                    if (node.isShutdown()) {
                        continue;
                    }

                    if (node.isDead()) {
                        float fact = (maxNodeFailures - node.getFailures()) / (float) Math.max(1, maxNodeFailures);

                        if (Math.random() >= fact) {
                            continue;
                        }
                    }

                    nodes.add(node);
                }

                return SelectStatus.CONTINUE;
            }
        });

        assert (nodes.size() <= count) : "Expected " + count + " or less elements but is " + nodes.size();
        return nodes;
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#getContacts()
     */
    public synchronized Collection<Contact> getContacts() {
        Collection<Contact> live = getActiveContacts();
        Collection<Contact> cached = getCachedContacts();

        List<Contact> nodes = new ArrayList<Contact>(live.size() + cached.size());
        nodes.addAll(live);
        nodes.addAll(cached);
        return nodes;
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#getActiveContacts()
     */
    public synchronized Collection<Contact> getActiveContacts() {
        List<Contact> nodes = new ArrayList<Contact>();
        for (Bucket bucket : bucketTrie.values()) {
            nodes.addAll(bucket.getActiveContacts());
        }
        return nodes;
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#getCachedContacts()
     */
    public synchronized Collection<Contact> getCachedContacts() {
        List<Contact> nodes = new ArrayList<Contact>();
        for (Bucket bucket : bucketTrie.values()) {
            nodes.addAll(bucket.getCachedContacts());
        }
        return nodes;
    }

    /*
     * If we are bootstrapping, we don't want to refresh the bucket
     * that contains the local node ID, as phase 1 already takes 
     * care of this. Additionally, when we bootstrap, we don't 
     * look at the bucket's timestamp (isRefreshRequired) so 
     * that we randomly fill up our routing table.
     * 
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#getRefreshIDs(boolean)
     */
    public synchronized Collection<KUID> getRefreshIDs(final boolean bootstrapping) {
        final KUID nodeId = getLocalNode().getNodeID();
        final List<KUID> randomIds = new ArrayList<KUID>();

        bucketTrie.select(nodeId, new Cursor<KUID, Bucket>() {
            public SelectStatus select(Entry<? extends KUID, ? extends Bucket> entry) {
                Bucket bucket = entry.getValue();

                // Don't refresh the local Bucket if we're bootstrapping
                // since phase one takes already care of it.
                if (bootstrapping && bucket.contains(getLocalNode().getNodeID())) {
                    return SelectStatus.CONTINUE;
                }

                if (bootstrapping || bucket.isRefreshRequired()) {
                    // Select a random ID with this prefix
                    KUID randomId = KUID.createPrefxNodeID(bucket.getBucketID(), bucket.getDepth());

                    if (LOG.isTraceEnabled()) {
                        LOG.trace("Refreshing bucket:" + bucket + " with random ID: " + randomId);
                    }

                    randomIds.add(randomId);
                    touchBucket(bucket);
                }

                return SelectStatus.CONTINUE;
            }
        });

        return randomIds;
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#getBuckets()
     */
    public synchronized Collection<Bucket> getBuckets() {
        return Collections.unmodifiableCollection(bucketTrie.values());
    }

    /**
     * Touches the given Bucket (i.e. updates its timeStamp).
     */
    private void touchBucket(Bucket bucket) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("Touching bucket: " + bucket);
        }

        bucket.touch();
    }

    /**
     * Pings the least recently seen active Contact in the given Bucket.
     */
    private void pingLeastRecentlySeenNode(Bucket bucket) {
        Contact lrs = bucket.getLeastRecentlySeenActiveContact();
        if (!isLocalNode(lrs)) {
            ping(lrs, null);
        }
    }

    /**
     * Pings the given Contact and adds the given DHTEventListener to
     * the DHTFuture if it's not null.
     */
    private void ping(Contact node, DHTFutureListener<PingResult> listener) {
        ContactPinger pinger = this.pinger;
        if (pinger != null) {
            pinger.ping(node, listener);
        } else {
            handleFailure(node.getNodeID(), node.getContactAddress());

            if (listener != null) {
                ExecutionException exception = new ExecutionException(
                        new DHTTimeoutException(node.getNodeID(), node.getContactAddress(), null, 0L));
                listener.handleExecutionException(exception);
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#getLocalNode()
     */
    public Contact getLocalNode() {
        if (localNode == null) {
            throw new IllegalStateException("RouteTable is not initialized");
        }
        return localNode;
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#isLocalNode(com.limegroup.mojito.Contact)
     */
    public boolean isLocalNode(Contact node) {
        return node.equals(getLocalNode());
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#size()
     */
    public synchronized int size() {
        return getActiveContacts().size() + getCachedContacts().size();
    }

    /*
     * (non-Javadoc)
     * @see com.limegroup.mojito.routing.RouteTable#clear()
     */
    public synchronized void clear() {
        bucketTrie.clear();
        fireClear();
        init();
    }

    /*
     * (non-Javadoc)
     * @see org.limewire.mojito.routing.RouteTable#purge(long)
     */
    public synchronized void purge(long elapsedTimeSinceLastContact) {
        if (localNode == null) {
            throw new IllegalStateException("RouteTable is not initialized");
        }

        if (elapsedTimeSinceLastContact == -1L) {
            return;
        }

        long currentTime = System.currentTimeMillis();
        for (Contact node : getActiveContacts()) {
            if (isLocalNode(node)) {
                continue;
            }

            if ((currentTime - node.getTimeStamp()) < elapsedTimeSinceLastContact) {
                continue;
            }

            remove(node);
        }

        for (Contact node : getCachedContacts()) {
            if ((currentTime - node.getTimeStamp()) < elapsedTimeSinceLastContact) {
                continue;
            }

            remove(node);
        }

        mergeBuckets();
    }

    /*
     * (non-Javadoc)
     * @see org.limewire.mojito.routing.RouteTable#purge(org.limewire.mojito.routing.RouteTable.PurgeMode, org.limewire.mojito.routing.RouteTable.PurgeMode[])
     */
    public synchronized void purge(PurgeMode first, PurgeMode... rest) {
        if (localNode == null) {
            throw new IllegalStateException("RouteTable is not initialized");
        }

        EnumSet<PurgeMode> modes = EnumSet.of(first, rest);

        if (modes.contains(PurgeMode.DROP_CACHE)) {
            dropCache();
        }

        if (modes.contains(PurgeMode.PURGE_CONTACTS)) {
            purgeContacts();
        }

        if (modes.contains(PurgeMode.MERGE_BUCKETS)) {
            mergeBuckets();
        }

        if (modes.contains(PurgeMode.STATE_TO_UNKNOWN)) {
            changeStateToUnknown(getActiveContacts());
            changeStateToUnknown(getCachedContacts());
        }
    }

    private synchronized void dropCache() {
        for (Contact node : getCachedContacts()) {
            remove(node);
        }
    }

    private synchronized void purgeContacts() {
        bucketTrie.traverse(new Cursor<KUID, Bucket>() {
            public SelectStatus select(Entry<? extends KUID, ? extends Bucket> entry) {
                Bucket bucket = entry.getValue();
                bucket.purge();
                return SelectStatus.CONTINUE;
            }
        });
    }

    private synchronized void mergeBuckets() {
        // Get the active Contacts
        Collection<Contact> activeNodes = getActiveContacts();
        activeNodes = ContactUtils.sortAliveToFailed(activeNodes);

        // Get the cached Contacts
        Collection<Contact> cachedNodes = getCachedContacts();
        cachedNodes = ContactUtils.sort(cachedNodes);

        // We count on the fact that getActiveContacts() and 
        // getCachedContacts() return copies!
        clear();

        // Remove the local Node from the List. Shouldn't fail as 
        // activeNodes is a copy!
        boolean removed = activeNodes.remove(localNode);
        assert (removed);

        // Re-add the active Contacts
        for (Contact node : activeNodes) {
            add(node);
        }

        // And re-add the cached Contacts
        for (Contact node : cachedNodes) {
            add(node);
        }
    }

    private synchronized void changeStateToUnknown(Collection<Contact> nodes) {
        for (Contact node : nodes) {
            node.unknown();
        }
    }

    protected void fireActiveContactAdded(Bucket bucket, Contact node) {
        fireRouteTableEvent(bucket, null, null, null, node, EventType.ADD_ACTIVE_CONTACT);
    }

    protected void fireCachedContactAdded(Bucket bucket, Contact existing, Contact node) {
        fireRouteTableEvent(bucket, null, null, existing, node, EventType.ADD_CACHED_CONTACT);
    }

    protected void fireContactUpdate(Bucket bucket, Contact existing, Contact node) {
        fireRouteTableEvent(bucket, null, null, existing, node, EventType.UPDATE_CONTACT);
    }

    protected void fireReplaceContact(Bucket bucket, Contact existing, Contact node) {
        fireRouteTableEvent(bucket, null, null, existing, node, EventType.REPLACE_CONTACT);
    }

    protected void fireRemoveContact(Bucket bucket, Contact node) {
        fireRouteTableEvent(bucket, null, null, null, node, EventType.REMOVE_CONTACT);
    }

    protected void fireContactCheck(Bucket bucket, Contact existing, Contact node) {
        fireRouteTableEvent(bucket, null, null, existing, node, EventType.CONTACT_CHECK);
    }

    protected void fireSplitBucket(Bucket bucket, Bucket left, Bucket right) {
        fireRouteTableEvent(bucket, left, right, null, null, EventType.SPLIT_BUCKET);
    }

    protected void fireClear() {
        fireRouteTableEvent(null, null, null, null, null, EventType.CLEAR);
    }

    protected void fireRouteTableEvent(Bucket bucket, Bucket left, Bucket right, Contact existing, Contact node,
            EventType type) {

        if (listeners.isEmpty()) {
            return;
        }

        final RouteTableEvent event = new RouteTableEvent(this, bucket, left, right, existing, node, type);

        Runnable r = new Runnable() {
            public void run() {
                for (RouteTableListener listener : listeners) {
                    listener.handleRouteTableEvent(event);
                }
            }
        };

        DHTExecutorService e = notifier;
        if (e != null)
            e.executeSequentially(r);
        else
            r.run();
    }

    @Override
    public synchronized String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("Local: ").append(getLocalNode()).append("\n");

        int alive = 0;
        int dead = 0;
        int down = 0;
        int unknown = 0;

        for (Bucket bucket : getBuckets()) {
            buffer.append(bucket).append("\n");

            for (Contact node : bucket.getActiveContacts()) {
                if (node.isShutdown()) {
                    down++;
                }

                if (node.isAlive()) {
                    alive++;
                } else if (node.isDead()) {
                    dead++;
                } else {
                    unknown++;
                }
            }

            for (Contact node : bucket.getCachedContacts()) {
                if (node.isShutdown()) {
                    down++;
                }

                if (node.isAlive()) {
                    alive++;
                } else if (node.isDead()) {
                    dead++;
                } else {
                    unknown++;
                }
            }
        }

        buffer.append("Total Buckets: ").append(bucketTrie.size()).append("\n");
        buffer.append("Total Active Contacts: ").append(getActiveContacts().size()).append("\n");
        buffer.append("Total Cached Contacts: ").append(getCachedContacts().size()).append("\n");
        buffer.append("Total Alive Contacts: ").append(alive).append("\n");
        buffer.append("Total Dead Contacts: ").append(dead).append("\n");
        buffer.append("Total Down Contacts: ").append(down).append("\n");
        buffer.append("Total Unknown Contacts: ").append(unknown).append("\n");
        return buffer.toString();
    }
}