package socialkademlia.operation;
import kademlia.message.Receiver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import kademlia.KadConfiguration;
import kademlia.KadServer;
import socialkademlia.dht.GetParameterFUC;
import kademlia.exceptions.RoutingException;
import kademlia.exceptions.UnknownMessageException;
import socialkademlia.exceptions.UpToDateContentException;
import socialkademlia.message.ContentLookupMessageFUC;
import kademlia.message.Message;
import kademlia.message.NodeReplyMessage;
import socialkademlia.message.UpToDateContentMessage;
import kademlia.node.KeyComparator;
import kademlia.node.Node;
import kademlia.operation.Operation;
import kademlia.util.RouteLengthChecker;
import socialkademlia.SocialKademliaNode;
import socialkademlia.dht.JSocialKademliaStorageEntry;
import socialkademlia.dht.SocialKademliaStorageEntry;
import socialkademlia.message.ContentMessage;
/**
* Looks up a specified identifier and returns the value associated with it
*
* @author Joshua Kissoon
* @since 20140422
*/
public class ContentLookupOperationFUC implements Operation, Receiver
{
/* Constants */
private static final Byte UNASKED = (byte) 0x00;
private static final Byte AWAITING = (byte) 0x01;
private static final Byte ASKED = (byte) 0x02;
private static final Byte FAILED = (byte) 0x03;
private final KadServer server;
private final SocialKademliaNode localNode;
private JSocialKademliaStorageEntry contentFound = null;
private final KadConfiguration config;
private final Message lookupMessage;
private boolean isContentFound;
private boolean newerContentExist = false; // Whether the content we have is up to date
private final SortedMap<Node, Byte> nodes;
/* Tracks messages in transit and awaiting reply */
private final Map<Integer, Node> messagesTransiting;
/* Used to sort nodes */
private final Comparator comparator;
/* Statistical information */
private final RouteLengthChecker routeLengthChecker;
private final GetParameterFUC params;
{
messagesTransiting = new HashMap<>();
isContentFound = false;
routeLengthChecker = new RouteLengthChecker();
}
/**
* @param server
* @param localNode
* @param params The parameters to search for the content which we need to find
* @param config
*/
public ContentLookupOperationFUC(KadServer server, SocialKademliaNode localNode, GetParameterFUC params, KadConfiguration config)
{
/* Construct our lookup message */
this.lookupMessage = new ContentLookupMessageFUC(localNode.getNode(), params);
this.server = server;
this.localNode = localNode;
this.config = config;
this.params = params;
/**
* We initialize a TreeMap to store nodes.
* This map will be sorted by which nodes are closest to the lookupId
*/
this.comparator = new KeyComparator(params.getKey());
this.nodes = new TreeMap(this.comparator);
}
/**
* @throws java.io.IOException
* @throws kademlia.exceptions.RoutingException
*/
@Override
public synchronized void execute() throws IOException, RoutingException
{
try
{
/* Set the local node as already asked */
nodes.put(this.localNode.getNode(), ASKED);
/**
* Check if we are a connection to the required content's owner and if we have it's node in our routing table
*/
if (this.localNode.getRoutingTable().containsConnection(this.params.getOwnerId()))
{
Node connNode = this.localNode.getRoutingTable().getConnectionNode(this.params.getOwnerId());
/* We only contact the owner of the contact if this is not the owner */
if (!connNode.equals(this.localNode.getNode()))
{
this.nodes.put(connNode, UNASKED);
}
}
/**
* We add all nodes here instead of the K-Closest because there may be the case that the K-Closest are offline
* - The operation takes care of looking at the K-Closest.
*/
List<Node> allNodes = this.localNode.getRoutingTable().getAllNodes();
this.addNodes(allNodes);
/* Also add the initial set of nodes to the routeLengthChecker */
this.routeLengthChecker.addInitialNodes(allNodes);
/**
* If we haven't found the requested amount of content as yet,
* keey trying until config.operationTimeout() time has expired
*/
int totalTimeWaited = 0;
int timeInterval = 10; // We re-check every n milliseconds
while (totalTimeWaited < this.config.operationTimeout())
{
if (!this.askNodesorFinish() && !isContentFound)
{
wait(timeInterval);
totalTimeWaited += timeInterval;
}
else
{
break;
}
}
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Add nodes from this list to the set of nodes to lookup
*
* @param list The list from which to add nodes
*/
public void addNodes(List<Node> list)
{
for (Node o : list)
{
/* If this node is not in the list, add the node */
if (!nodes.containsKey(o))
{
nodes.put(o, UNASKED);
}
}
}
/**
* Asks some of the K closest nodes seen but not yet queried.
* Assures that no more than DefaultConfiguration.CONCURRENCY messages are in transit at a time
*
* This method should be called every time a reply is received or a timeout occurs.
*
* If all K closest nodes have been asked and there are no messages in transit,
* the algorithm is finished.
*
* @return <code>true</code> if finished OR <code>false</code> otherwise
*/
private boolean askNodesorFinish() throws IOException
{
/* If >= CONCURRENCY nodes are in transit, don't do anything */
if (this.config.maxConcurrentMessagesTransiting() <= this.messagesTransiting.size())
{
return false;
}
/* Get unqueried nodes among the K closest seen that have not FAILED */
List<Node> unasked = this.closestNodesNotFailed(UNASKED);
if (unasked.isEmpty() && this.messagesTransiting.isEmpty())
{
/* We have no unasked nodes nor any messages in transit, we're finished! */
return true;
}
/* Sort nodes according to criteria */
Collections.sort(unasked, this.comparator);
/**
* Send messages to nodes in the list;
* making sure than no more than CONCURRENCY messsages are in transit
*/
for (int i = 0; (this.messagesTransiting.size() < this.config.maxConcurrentMessagesTransiting()) && (i < unasked.size()); i++)
{
Node n = (Node) unasked.get(i);
int comm = server.sendMessage(n, lookupMessage, this);
this.nodes.put(n, AWAITING);
this.messagesTransiting.put(comm, n);
}
/* We're not finished as yet, return false */
return false;
}
/**
* Find The K closest nodes to the target lookupId given that have not FAILED.
* From those K, get those that have the specified status
*
* @param status The status of the nodes to return
*
* @return A List of the closest nodes
*/
private List<Node> closestNodesNotFailed(Byte status)
{
List<Node> closestNodes = new ArrayList<>(this.config.k());
int remainingSpaces = this.config.k();
for (Map.Entry e : this.nodes.entrySet())
{
if (!FAILED.equals(e.getValue()))
{
if (status.equals(e.getValue()))
{
/* We got one with the required status, now add it */
closestNodes.add((Node) e.getKey());
}
if (--remainingSpaces == 0)
{
break;
}
}
}
return closestNodes;
}
@Override
public synchronized void receive(Message incoming, int comm) throws IOException, RoutingException
{
if (this.isContentFound)
{
return;
}
if (incoming instanceof ContentMessage)
{
/* The reply received is a content message with the required content, take it in */
ContentMessage msg = (ContentMessage) incoming;
/* ContentMessage node to our routing table */
this.localNode.getRoutingTable().insert(msg.getOrigin());
/* Get the Content and check if it satisfies the required parameters */
JSocialKademliaStorageEntry content = msg.getContent();
this.contentFound = content;
this.isContentFound = true;
this.newerContentExist = true;
}
else if (incoming instanceof UpToDateContentMessage)
{
/**
* The content we have is up to date
* No sense in adding our own content to the resultset, so lets just exit
*/
this.newerContentExist = false;
this.isContentFound = true;
}
else if (incoming instanceof NodeReplyMessage)
{
/* The reply received is a NodeReplyMessage with nodes closest to the content needed */
NodeReplyMessage msg = (NodeReplyMessage) incoming;
/* Add the origin node to our routing table */
Node origin = msg.getOrigin();
this.localNode.getRoutingTable().insert(origin);
/* Set that we've completed ASKing the origin node */
this.nodes.put(origin, ASKED);
/* Remove this msg from messagesTransiting since it's completed now */
this.messagesTransiting.remove(comm);
/* Add the received nodes to the routeLengthChecker */
this.routeLengthChecker.addNodes(msg.getNodes(), origin);
/* Add the received nodes to our nodes list to query */
this.addNodes(msg.getNodes());
this.askNodesorFinish();
}
else
{
/* @todo Something is wrong that we get messages of other types here, investigate it! */
System.err.println(this.localNode.getNode() + " Got a response message of type " + incoming.getClass() + " from " + ((ContentLookupMessageFUC) incoming).getOrigin() + " in ContentLookupOperationFUC. ");
}
}
/**
* A node does not respond or a packet was lost, we set this node as failed
*
* @param comm
*
* @throws java.io.IOException
*/
@Override
public synchronized void timeout(int comm) throws IOException
{
/* Get the node associated with this communication */
Node n = this.messagesTransiting.get(new Integer(comm));
if (n == null)
{
throw new UnknownMessageException("Unknown comm: " + comm);
}
/* Mark this node as failed and inform the routing table that it's unresponsive */
this.nodes.put(n, FAILED);
this.localNode.getRoutingTable().setUnresponsiveContact(n);
this.messagesTransiting.remove(comm);
this.askNodesorFinish();
}
/**
* @return Whether the content was found or not.
*/
public boolean isContentFound()
{
return this.isContentFound;
}
/**
* @return The list of all content found during the lookup operation
*
* @throws kademlia.exceptions.UpToDateContentException
*/
public JSocialKademliaStorageEntry getContentFound() throws UpToDateContentException
{
/* Check if we have newer content */
if (this.newerContentExist)
{
return this.contentFound;
}
else
{
throw new UpToDateContentException("The content is up to date");
}
}
/**
* @return Boolean whether newer content exist or not
*/
public boolean newerContentExist()
{
return this.newerContentExist;
}
/**
* @return How many hops it took in order to get to the content.
*/
public int routeLength()
{
return this.routeLengthChecker.getRouteLength();
}
}