com.offbynull.voip.kademlia.model.RouteTreeNode.java Source code

Java tutorial

Introduction

Here is the source code for com.offbynull.voip.kademlia.model.RouteTreeNode.java

Source

/*
 * Copyright (c) 2015, Kasra Faghihi, All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.offbynull.voip.kademlia.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.lang3.Validate;

final class RouteTreeNode {

    private final BitString prefix;
    private final int suffixLen;
    private final List<RouteTreeBranch> branches; // branches can contain KBuckets or RouteTreeLevels that are further down

    public RouteTreeNode(BitString prefix, int suffixLen, KBucket[] buckets) {
        Validate.notNull(prefix);
        Validate.notNull(buckets);
        Validate.noNullElements(buckets);
        Validate.isTrue(suffixLen > 0);

        this.prefix = prefix;
        this.suffixLen = suffixLen;
        this.branches = new ArrayList<>(buckets.length);
        Arrays.stream(buckets).map(x -> new RouteTreeBucketBranch(x)).forEachOrdered(branches::add);
    }

    public RouteTreeBranch getBranch(int idx) {
        Validate.isTrue(idx >= 0);
        Validate.isTrue(idx < branches.size());

        return branches.get(idx);
    }

    public void setBranch(int idx, RouteTreeBranch branch) {
        Validate.notNull(branch);
        Validate.isTrue(idx >= 0);
        Validate.isTrue(idx < branches.size());

        branches.set(idx, branch);
    }

    public int getBranchCount() {
        return branches.size();
    }

    public BitString getPrefix() {
        return prefix;
    }

    // id is the id we're trying to find
    // treeset compares against id
    public void findNodesWithLargestPossiblePrefix(Id id, TreeSet<Activity> output, int max, boolean includeStale) {
        Validate.notNull(id);
        Validate.notNull(output); // technically shouldn't contain any null elements, but we don't care since we're just adding to this
        Validate.isTrue(max >= 0); // why would anyone want 0? let thru anyways
        Validate.isTrue(id.getBitString().getBits(0, prefix.getBitLength()).equals(prefix)); // ensure prefix matches

        // Recursively go down the until you find the branch with the largest matching prefix to ID. Once you find it, call
        // dumpAllNodesUnderTreeNode, and as you pop back up call dumpAllNodesUnderTreeNode again (making sure to not recurse back in to
        // the branch you're coming out of).

        int traverseIdx = (int) id.getBitsAsLong(prefix.getBitLength(), suffixLen);
        RouteTreeBranch traverseBranch = branches.get(traverseIdx);
        BitString traversePrefix = traverseBranch.getPrefix(); //id.getBitString().getBits(0, prefix.getBitLength() + suffixLen);

        if (traverseBranch instanceof RouteTreeNodeBranch) {
            RouteTreeNode treeNode = traverseBranch.getItem();
            treeNode.findNodesWithLargestPossiblePrefix(id, output, max, includeStale);

            dumpAllNodesUnderTreeNode(id, output, max, includeStale, singleton(traversePrefix));
        } else if (traverseBranch instanceof RouteTreeBucketBranch) {
            dumpAllNodesUnderTreeNode(id, output, max, includeStale, emptySet());
        } else {
            throw new IllegalStateException(); // should never happen
        }
    }

    // id is the id we're trying to find
    // treeset compares against id
    public void dumpAllNodesUnderTreeNode(Id id, TreeSet<Activity> output, int max, boolean includeStale,
            Set<BitString> skipPrefixes) {
        Validate.notNull(id);
        Validate.notNull(output); // technically shouldn't contain any null elements, but we don't care since we're just adding to this
        Validate.notNull(skipPrefixes);
        Validate.noNullElements(skipPrefixes);
        Validate.isTrue(max >= 0); // why would anyone want 0 here? let thru anwyways

        // No more room in bucket? just leave right away.
        if (output.size() >= max) {
            return;
        }

        // Sort branches at this treenode by how close the are to the ID we're searching for... Go through the sorted branches in
        // order...
        //
        //   If it's a bucket: dump it.
        //   If it's a branch: recurse in to the branch and repeat
        //
        ArrayList<RouteTreeBranch> sortedBranches = new ArrayList<>(branches);
        Collections.sort(sortedBranches, new PrefixClosenessComparator(id, prefix.getBitLength(), suffixLen));

        // What is the point of taking in an ID and sorting the branches in this tree node such that the we access the "closer" prefixes
        // first? We want to access the branches that are closer to the suffix of the ID first because ...
        //
        //
        // 1. Given the same prefix, we don't end up accessing the exact same set of nodes given. For example...
        //
        //      0/\1
        //      /  EMPTY
        //    0/\1
        //    /  FULL
        //  0/\1
        // ME  FULL
        //
        // Assume the routing tree above. We want to route to node 111, but bucket 1xx is empty. We then go down the other branch and
        // start grabbing nodes starting with prefix 0xx. We then use the suffix of 111 (x11) to determine which branches to traverse
        // down first for our 0xx nodes to return. We do this because we don't want to return the same set of nodes everytime someone
        // tries to access a 1xx node and we have an empty branch.
        //
        // For example...
        // if someone wanted 111 and 1xx was empty, path to search under 0xx would be 011, then 001, then 000.
        // if someone wanted 101 and 1xx was empty, path to search under 0xx would be 001, then 000, then 011.
        //
        // If we did something like a depth-first search, we'd always target 000 first, then 001, then 011. We don't want to do that
        // because we don't want to return the same set of nodes everytime. It would end up being an undue burden on those nodes.
        //
        //
        //
        // 2. Remember our notion of closeness: XOR and normal integer less-than to see which is closer. So for example, lets say we're
        // looking for ID 111110 and the prefix at this point in the tree is is 110xxx. Even though the prefix 110 doesn't match, we
        // still want to match as closely to the remaining suffix as possible, because when we XOR those extra 0's at the beginning of 
        // the suffix mean that we're closer.
        //
        // For example...
        //
        // This tree node has the prefix 110xxx and the ID we're searching for is 111110. There are 2 branches at this tree node:
        // 1100xx and 1101xx
        //
        //      110xxx
        //        /\
        //       /  \
        //      /    \
        //   0 /      \ 1
        //    /        \
        // 1100xx    1101xx
        //
        // We know that for ID 111110, the IDs under 1101xx WILL ALWAYS BE CLOSER than the IDs at 1100xx.
        //
        // XORing with the 1100xx bucket ... XOR(111110, 1100xx) = 0011xx
        // 
        // XORing with the 1101xx bucket ... XOR(111110, 1101xx) = 0010xx
        //
        //
        //     Remember how < works... go compare each single bit from the beginning until you come across a pair of bits that aren't
        //     equal (one is 0 and the other is 1). The ID with 0 at that position is less-than the other one.
        //
        //
        // The one on the bottom (1101xx) will ALWAYS CONTAIN CLOSER IDs...
        //
        // An example ID in top:    110011 ... XOR(111110, 110011) = 001101 = 13
        // An exmaple ID in bottom: 110100 ... XOR(111110, 110100) = 001010 = 9
        // 

        for (RouteTreeBranch sortedBranch : sortedBranches) {
            if (skipPrefixes.contains(sortedBranch.getPrefix())) {
                continue;
            }

            if (sortedBranch instanceof RouteTreeNodeBranch) {
                RouteTreeNode node = sortedBranch.getItem();
                node.dumpAllNodesUnderTreeNode(id, output, max, includeStale, emptySet()); // dont propogate skipPrefixes (not relevant)

                // Bucket's full after dumping nodes in that branch. No point in continued processing.
                if (output.size() >= max) {
                    return;
                }
            } else if (sortedBranch instanceof RouteTreeBucketBranch) {
                KBucket bucket = sortedBranch.getItem();

                // don't bother with locked nodes for now, we're not supporting them
                output.addAll(bucket.dumpBucket(true, includeStale, false));

                // Bucket's full after that add. No point in continued processing.
                if (output.size() >= max) {
                    // If we have more than max elements from that last add, start evicting farthest away nodes
                    while (output.size() > max) {
                        output.pollLast();
                    }
                    return;
                }
            } else {
                throw new IllegalStateException(); // should never happen
            }
        }
    }

    public KBucket getBucketForPrefix(BitString searchPrefix) {
        Validate.notNull(searchPrefix);
        Validate.isTrue(searchPrefix.getBits(0, prefix.getBitLength()).equals(prefix)); // ensure prefix of searchPrefix matches

        int bucketIdx = (int) searchPrefix.getBitsAsLong(prefix.getBitLength(), suffixLen);
        RouteTreeBranch branch = branches.get(bucketIdx);

        if (branch instanceof RouteTreeNodeBranch) {
            RouteTreeNode node = branch.getItem();
            return node.getBucketForPrefix(searchPrefix);
        } else if (branch instanceof RouteTreeBucketBranch) {
            KBucket bucket = branch.getItem();
            return bucket;
        } else {
            throw new IllegalStateException(); // should never happen
        }
    }

    public KBucket getBucketFor(Id id) {
        Validate.notNull(id);
        Validate.isTrue(id.getBitString().getBits(0, prefix.getBitLength()).equals(prefix)); // ensure prefix matches

        int bucketIdx = (int) id.getBitsAsLong(prefix.getBitLength(), suffixLen);
        RouteTreeBranch branch = branches.get(bucketIdx);

        if (branch instanceof RouteTreeNodeBranch) {
            RouteTreeNode treeNode = branch.getItem();
            return treeNode.getBucketFor(id);
        } else if (branch instanceof RouteTreeBucketBranch) {
            KBucket bucket = branch.getItem();
            return bucket;
        } else {
            throw new IllegalStateException(); // should never happen
        }
    }

    public void dumpAllBucketPrefixes(List<BitString> output) {
        Validate.notNull(output); // technically shouldn't contain any null elements, but we don't care since we're just adding to this

        for (RouteTreeBranch branch : branches) {
            if (branch instanceof RouteTreeNodeBranch) {
                RouteTreeNode treeNode = branch.getItem();
                treeNode.dumpAllBucketPrefixes(output);
            } else if (branch instanceof RouteTreeBucketBranch) {
                output.add(branch.getPrefix());
            } else {
                throw new IllegalStateException(); // should never happen
            }
        }
    }

    private static final class PrefixClosenessComparator implements Comparator<RouteTreeBranch>, Serializable {
        private static final long serialVersionUID = 1L;

        // This is a hacky way to compare bitstrings using the XOR metric intended for IDs
        private final int prefixLen;
        private final int suffixLen;
        private final IdXorMetricComparator partialIdClosenessComparator;

        public PrefixClosenessComparator(Id id, int prefixLen, int suffixLen) {
            Validate.notNull(id);
            Validate.isTrue(prefixLen >= 0);
            Validate.isTrue(suffixLen > 0);
            Validate.isTrue(id.getBitLength() >= prefixLen + suffixLen);

            this.prefixLen = prefixLen;
            this.suffixLen = suffixLen;

            Id partialId = Id.create(id.getBitString().getBits(prefixLen, suffixLen));
            this.partialIdClosenessComparator = new IdXorMetricComparator(partialId);
        }

        @Override
        public int compare(RouteTreeBranch o1, RouteTreeBranch o2) {
            Validate.isTrue(o1.getPrefix().getBitLength() == prefixLen + suffixLen);
            Validate.isTrue(o2.getPrefix().getBitLength() == prefixLen + suffixLen);

            return partialIdClosenessComparator.compare(Id.create(o1.getPrefix().getBits(prefixLen, suffixLen)),
                    Id.create(o2.getPrefix().getBits(prefixLen, suffixLen)));
        }
    }
}