com.offbynull.voip.kademlia.GraphHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.offbynull.voip.kademlia.GraphHelper.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;

import com.offbynull.peernetic.core.actor.Context;
import com.offbynull.peernetic.core.shuttle.Address;
import com.offbynull.peernetic.visualizer.gateways.graph.AddEdge;
import com.offbynull.peernetic.visualizer.gateways.graph.AddNode;
import com.offbynull.peernetic.visualizer.gateways.graph.LabelNode;
import com.offbynull.peernetic.visualizer.gateways.graph.MoveNode;
import com.offbynull.peernetic.visualizer.gateways.graph.StyleNode;
import com.offbynull.voip.kademlia.model.Activity;
import com.offbynull.voip.kademlia.model.BitString;
import com.offbynull.voip.kademlia.model.Id;
import com.offbynull.voip.kademlia.model.IdXorMetricComparator;
import com.offbynull.voip.kademlia.model.RouteTreeChangeSet;
import com.offbynull.voip.kademlia.model.Router;
import com.offbynull.voip.kademlia.model.RouterChangeSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeSet;
import org.apache.commons.lang3.Validate;

final class GraphHelper {

    private static final String TREE_ROOT_LABEL = "ROOT";
    private static final int TREE_ROOT_COLOR = 0x7F7F7F;
    private static final int TREE_BUCKET_COLOR = 0x7F7F7F;

    private static final double Y_SPREAD = 50.0;
    private static final double X_SPREAD = 50.0;

    private final Address graphAddress;

    private final Id baseId;
    private final LinkedHashSet<BitString> routerBucketPrefixes;
    private final LinkedHashMap<BitString, SortedSet<Id>> routeTreePrefixToIds; // ids assigned to each tree prefix
    private final TreeSet<Id> nearBucketIds;

    public GraphHelper(Id baseId, Address graphAddress, Router router) {
        Validate.notNull(baseId);
        Validate.notNull(graphAddress);
        Validate.notNull(router);
        this.baseId = baseId;
        this.graphAddress = graphAddress.appendSuffix(baseId.getBitString().toString());

        List<BitString> prefixes = router.dumpBucketPrefixes();
        Collections.sort(prefixes, (x, y) -> Integer.compare(x.getBitLength(), y.getBitLength()));

        this.routerBucketPrefixes = new LinkedHashSet<>(); // order matters, smallest prefixes first
        this.routerBucketPrefixes.addAll(prefixes);

        this.routeTreePrefixToIds = new LinkedHashMap<>(); // order matters, smallest prefixes first

        IdXorMetricComparator idComparator = new IdXorMetricComparator(baseId);
        this.nearBucketIds = new TreeSet<>(idComparator); // order matters, based on closeness of ids
    }

    public void createGraphs(Context ctx) {
        setupRoutingTreeGraph(ctx);
    }

    public void applyRouterChanges(Context ctx, RouterChangeSet changeSet) {
        applyRoutingTreeChanges(ctx, changeSet.getRouteTreeChangeSet());
    }

    @SuppressWarnings("unchecked")
    private void applyRoutingTreeChanges(Context ctx, RouteTreeChangeSet changeSet) {
        Set<BitString> updatedPrefixes = new HashSet<>();

        // Remove nodes from graph and set
        for (Activity removedNode : changeSet.getKBucketChangeSet().getBucketChangeSet().viewRemoved()) {
            Id id = removedNode.getNode().getId();
            BitString prefix = findPrefixInRouteTreeIds(id);
            routeTreePrefixToIds.get(prefix).remove(id);
            updatedPrefixes.add(prefix);
        }

        // Add nodes to graph and set
        for (Activity addedNode : changeSet.getKBucketChangeSet().getBucketChangeSet().viewAdded()) {
            Id id = addedNode.getNode().getId();
            BitString prefix = findPrefixInRouteTreeIds(id);
            routeTreePrefixToIds.get(prefix).add(id);
            updatedPrefixes.add(prefix);
        }

        for (BitString updatedPrefix : updatedPrefixes) {
            // Concatenate to string and set as label
            StringJoiner joiner = new StringJoiner("\n");

            // Find parent
            int prefixBitLength = updatedPrefix.getBitLength();
            BitString parentPrefix = getParentPrefix(routeTreePrefixToIds.keySet(), updatedPrefix);

            // Calculate number of bits after parent prefix (the bits to display)
            int newBitsOffset = parentPrefix.getBitLength();
            int newBitsLength = prefixBitLength - parentPrefix.getBitLength();
            BitString prefixDisplayBits = updatedPrefix.getBits(newBitsOffset, newBitsLength);

            // Change label to contain all nodes in the bucket
            joiner.add(prefixDisplayBits.toString());
            routeTreePrefixToIds.get(updatedPrefix).forEach(id -> joiner.add(id.getBitString().toString()));
            ctx.addOutgoingMessage(graphAddress, new LabelNode(updatedPrefix.toString(), joiner.toString()));
        }
    }

    private BitString findPrefixInRouteTreeIds(Id id) {
        int len = id.getBitLength();
        BitString routeTreeNode = id.getBitString().getBits(0, len);

        while (!routeTreePrefixToIds.containsKey(routeTreeNode)) {
            len--;
            routeTreeNode = id.getBitString().getBits(0, len);
        }

        return routeTreeNode;
    }

    private void setupRoutingTreeGraph(Context ctx) {
        IdXorMetricComparator idComparator = new IdXorMetricComparator(baseId);

        Map<BitString, Point> processedPrefixes = new HashMap<>(); // prefix -> position on graph
        addRootToGraph(ctx, processedPrefixes);
        routeTreePrefixToIds.put(BitString.createFromString(""), new TreeSet<>(idComparator)); // special case for empty prefix

        LinkedList<BitString> tempPrefixes = new LinkedList<>(routerBucketPrefixes);

        double maxYPosition = Double.MIN_VALUE;
        while (true) {
            // Get next prefixes
            ArrayList<BitString> nextLevelPrefixes = removePrefixesForNextLevel(tempPrefixes);
            if (nextLevelPrefixes.isEmpty()) {
                break;
            }

            // Find parent
            int bitLengthOfNextLevelPrefixes = nextLevelPrefixes.get(0).getBitLength();
            BitString parentPrefix = getParentPrefix(processedPrefixes.keySet(), nextLevelPrefixes.get(0));
            Point parentPoint = processedPrefixes.get(parentPrefix);

            // Calculate number of bits after parent prefix (the bits to display)
            int newBitsOffset = parentPrefix.getBitLength();
            int newBitsLength = bitLengthOfNextLevelPrefixes - parentPrefix.getBitLength();

            // Calculate starting x and y positions
            double numBranches = 1 << newBitsLength;
            double missingBitLength = baseId.getBitLength() - bitLengthOfNextLevelPrefixes;
            double ySpreadAtLevel = Y_SPREAD * (missingBitLength + 1.0);
            double xSpreadAtLevel = X_SPREAD * (missingBitLength + 1.0);
            double yPosition = parentPoint.y + ySpreadAtLevel; // nodes right below the parent
            double xPosition = parentPoint.x - xSpreadAtLevel * (numBranches - 1.0) / 2.0; // nodes x-centered on parent

            // special-case for netLevelPrefixes where prefix for our baseId doesn't exist... this is the branch that falls further down
            //
            // e.g. If you're 000, prefixes will be 1, 01, 001, 000... but for each of those you'll want to show the falling-thru prefix as
            // well.. for example...
            //
            //                                    /\
            //        (NOT STATED IN PREFIXES) 0 /  \ 1
            //                                  /\
            //     (NOT STATED IN PREFIXES) 00 /  \ 01
            //                                /\
            //      (EXISTS IN PREFIXES) 000 /  \ 001
            //
            // Note that 000 exists, but 00 and 0 don't.
            BitString baseIdPortion = baseId.getBitString().getBits(0, bitLengthOfNextLevelPrefixes);
            if (!nextLevelPrefixes.contains(baseIdPortion)) {
                nextLevelPrefixes.add(baseIdPortion);
            }

            // Make sure smallest branch is always to the left-most by sorting
            Collections.sort(nextLevelPrefixes,
                    (x, y) -> Long.compare(x.getBitsAsLong(newBitsOffset, newBitsLength),
                            y.getBitsAsLong(newBitsOffset, newBitsLength)));

            // Add prefixes from routing tree
            for (BitString nextPrefix : nextLevelPrefixes) {
                routeTreePrefixToIds.put(nextPrefix, new TreeSet<>(idComparator));

                addPrefixToGraph(nextPrefix, newBitsOffset, newBitsLength, xPosition, yPosition, ctx, parentPrefix,
                        processedPrefixes);
                xPosition += xSpreadAtLevel;
            }

            // Update max Y position
            maxYPosition = Math.max(maxYPosition, yPosition);
        }
    }

    private void addRootToGraph(Context ctx, Map<BitString, Point> processedPrefixes) {
        BitString id = BitString.createFromString("");
        ctx.addOutgoingMessage(graphAddress, new AddNode(id.toString()));
        ctx.addOutgoingMessage(graphAddress, new LabelNode(id.toString(), TREE_ROOT_LABEL));
        ctx.addOutgoingMessage(graphAddress, new MoveNode(id.toString(), 0.0, 0.0));
        ctx.addOutgoingMessage(graphAddress, new StyleNode(id.toString(), TREE_ROOT_COLOR));
        processedPrefixes.put(id, new Point(0.0, 0.0));
    }

    private void addPrefixToGraph(BitString nextPrefix, int newBitsOffset, int newBitsLength, double xPosition,
            double yPosition, Context ctx, BitString parentId, Map<BitString, Point> processedPrefixes) {
        BitString displayBits = nextPrefix.getBits(newBitsOffset, newBitsLength);
        Point displayPoint = new Point(xPosition, yPosition);
        ctx.addOutgoingMessage(graphAddress, new AddNode(nextPrefix.toString()));
        ctx.addOutgoingMessage(graphAddress, new LabelNode(nextPrefix.toString(), displayBits.toString()));
        ctx.addOutgoingMessage(graphAddress, new MoveNode(nextPrefix.toString(), displayPoint.x, displayPoint.y));
        ctx.addOutgoingMessage(graphAddress, new StyleNode(nextPrefix.toString(), TREE_BUCKET_COLOR));
        ctx.addOutgoingMessage(graphAddress, new AddEdge(parentId.toString(), nextPrefix.toString()));
        processedPrefixes.put(nextPrefix, displayPoint);
    }

    private BitString getParentPrefix(Set<BitString> treePrefixes, BitString checkBitString) {
        do {
            checkBitString = checkBitString.getBits(0, checkBitString.getBitLength() - 1);
            if (treePrefixes.contains(checkBitString)) {
                return checkBitString;
            }
        } while (checkBitString.getBitLength() >= 0);

        throw new IllegalStateException(); // should never happen
    }

    private ArrayList<BitString> removePrefixesForNextLevel(LinkedList<BitString> sortedPrefixes) {
        ArrayList<BitString> ret = new ArrayList<>();

        if (sortedPrefixes.isEmpty()) {
            return ret;
        }

        int hitCount = sortedPrefixes.peekFirst().getBitLength();

        while (!sortedPrefixes.isEmpty()) {
            if (sortedPrefixes.peekFirst().getBitLength() == hitCount) {
                ret.add(sortedPrefixes.removeFirst());
            } else {
                break;
            }
        }

        return ret;
    }

    private static final class Point {
        private final double x;
        private final double y;

        public Point(double x, double y) {
            this.x = x;
            this.y = y;
        }

    }
}