package socialkademlia; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.InetAddress; import java.util.NoSuchElementException; import java.util.Timer; import java.util.TimerTask; import kademlia.DefaultConfiguration; import kademlia.dht.GetParameter; import kademlia.KadConfiguration; import kademlia.KadServer; import socialkademlia.dht.JSocialKademliaDHT; import socialkademlia.dht.GetParameterFUC; import kademlia.dht.KadContent; import kademlia.dht.KademliaStorageEntry; import socialkademlia.dht.JSocialKademliaStorageEntry; import socialkademlia.dht.util.StorageEntryCompressor; import kademlia.exceptions.ContentNotFoundException; import kademlia.exceptions.RoutingException; import kademlia.message.KademliaMessageFactory; import socialkademlia.exceptions.UpToDateContentException; import socialkademlia.message.MessageFactory; import kademlia.node.Node; import kademlia.node.KademliaId; import socialkademlia.operation.ConnectOperation; import socialkademlia.operation.ContentLookupOperation; import socialkademlia.operation.ContentLookupOperationFUC; import kademlia.operation.Operation; import socialkademlia.operation.KadRefreshOperation; import socialkademlia.operation.StoreOperation; import socialkademlia.routing.SocialKademliaRoutingTable; import socialkademlia.routing.JSocialKademliaRoutingTable; import kademlia.util.serializer.JsonSerializer; import socialkademlia.dht.SocialKademliaDHT; import socialkademlia.dht.SocialKademliaStorageEntry; import socialkademlia.util.serializer.JsonSocialKadRoutingTableSerializer; import socialkademlia.util.serializer.JsonSocialKademliaDHTSerializer; /** * The main Kademlia Node on the network, this node manages everything for this local system. * * @author Joshua Kissoon * @since 20140215 * * @todo When we receive a store message - if we have a newer version of the content, re-send this newer version to that node so as to update their version * @todo Handle IPv6 Addresses * */ public class JSocialKademliaNode implements SocialKademliaNode { /* Kademlia Attributes */ private final String ownerId; /* Objects to be used */ private final transient Node localNode; private final transient KadServer server; private final transient SocialKademliaDHT dht; private transient SocialKademliaRoutingTable routingTable; private final int udpPort; private transient KadConfiguration config; /* Timer used to execute refresh operations */ private transient Timer refreshOperationTimer; private transient TimerTask refreshOperationTTask; /* Factories */ private final transient KademliaMessageFactory messageFactory; /* Statistics */ private final transient SocialKadStatistician statistician; { statistician = new Statistician(); } /** * Creates a Kademlia DistributedMap using the specified name as filename base. * If the id cannot be read from disk the specified defaultId is used. * The instance is bootstraped to an existing network by specifying the * address of a bootstrap node in the network. * * @param ownerId The Name of this node used for storage * @param localNode The Local Node for this Kad instance * @param udpPort The UDP port to use for routing messages * @param dht The JSocialKademliaDHT for this instance * @param config * @param routingTable * * @throws IOException If an error occurred while reading id or local map * from disk <i>or</i> a network error occurred while * attempting to bootstrap to the network * */ public JSocialKademliaNode(String ownerId, Node localNode, int udpPort, SocialKademliaDHT dht, SocialKademliaRoutingTable routingTable, KadConfiguration config) throws IOException { this.ownerId = ownerId; this.udpPort = udpPort; this.localNode = localNode; this.dht = dht; this.config = config; this.routingTable = routingTable; this.messageFactory = new MessageFactory(this, this.dht, this.config); this.server = new KadServer(udpPort, this.messageFactory, this.localNode, this.config, this.statistician); this.startRefreshOperation(); } /** * Schedule the recurring refresh operation */ @Override public final void startRefreshOperation() { this.refreshOperationTimer = new Timer(true); refreshOperationTTask = new TimerTask() { @Override public void run() { try { /* Runs a JSocialKademliaDHT RefreshOperation */ JSocialKademliaNode.this.refresh(); } catch (IOException e) { System.err.println("KademliaNode: Refresh Operation Failed; Message: " + e.getMessage()); } } }; refreshOperationTimer.schedule(refreshOperationTTask, this.config.restoreInterval(), this.config.restoreInterval()); } @Override public final void stopRefreshOperation() { /* Close off the timer tasks */ this.refreshOperationTTask.cancel(); this.refreshOperationTimer.cancel(); this.refreshOperationTimer.purge(); } public JSocialKademliaNode(String ownerId, Node node, int udpPort, SocialKademliaRoutingTable routingTable, KadConfiguration config) throws IOException { this( ownerId, node, udpPort, new JSocialKademliaDHT(ownerId, config), routingTable, config ); } public JSocialKademliaNode(String ownerId, Node node, int udpPort, KadConfiguration config) throws IOException { this( ownerId, node, udpPort, new JSocialKademliaRoutingTable(node, config), config ); } public JSocialKademliaNode(String ownerId, KademliaId defaultId, int udpPort) throws IOException { this( ownerId, new Node(defaultId, InetAddress.getLocalHost(), udpPort), udpPort, new DefaultConfiguration() ); } /** * Load Stored state using default configuration * * @param ownerId The ID of the owner for the stored state * * @return A Kademlia instance loaded from a stored state in a file * * @throws java.io.FileNotFoundException * @throws java.lang.ClassNotFoundException */ public static JSocialKademliaNode loadFromFile(String ownerId) throws FileNotFoundException, IOException, ClassNotFoundException { return JSocialKademliaNode.loadFromFile(ownerId, new DefaultConfiguration()); } /** * Load Stored state * * @param ownerId The ID of the owner for the stored state * @param iconfig Configuration information to work with * * @return A Kademlia instance loaded from a stored state in a file * * @throws java.io.FileNotFoundException * @throws java.lang.ClassNotFoundException */ public static JSocialKademliaNode loadFromFile(String ownerId, KadConfiguration iconfig) throws FileNotFoundException, IOException, ClassNotFoundException { DataInputStream din; /** * @section Read Basic Kad data */ din = new DataInputStream(new FileInputStream(getStateStorageFolderName(ownerId, iconfig) + File.separator + "kad.kns")); JSocialKademliaNode ikad = new JsonSerializer<JSocialKademliaNode>().read(din); /** * @section Read the routing table */ din = new DataInputStream(new FileInputStream(getStateStorageFolderName(ownerId, iconfig) + File.separator + "routingtable.kns")); SocialKademliaRoutingTable irtbl = new JsonSocialKadRoutingTableSerializer(iconfig).read(din); /** * @section Read the node state */ din = new DataInputStream(new FileInputStream(getStateStorageFolderName(ownerId, iconfig) + File.separator + "node.kns")); Node inode = new JsonSerializer<Node>().read(din); /** * @section Read the DHT */ din = new DataInputStream(new FileInputStream(getStateStorageFolderName(ownerId, iconfig) + File.separator + "dht.kns")); SocialKademliaDHT idht = new JsonSocialKademliaDHTSerializer().read(din); idht.setConfiguration(iconfig); return new JSocialKademliaNode(ownerId, inode, ikad.getPort(), idht, irtbl, iconfig); } /** * @return Node The local node for this system */ @Override public Node getNode() { return this.localNode; } /** * @return The KadServer used to send/receive messages */ @Override public KadServer getServer() { return this.server; } /** * @return The JSocialKademliaDHT for this kad instance */ @Override public SocialKademliaDHT getDHT() { return this.dht; } /** * @return The current KadConfiguration object being used */ @Override public KadConfiguration getCurrentConfiguration() { return this.config; } /** * Connect to an existing peer-to-peer network. * * @param n The known node in the peer-to-peer network * * @throws RoutingException If the bootstrap node could not be contacted * @throws IOException If a network error occurred * @throws IllegalStateException If this object is closed * */ @Override public synchronized final void bootstrap(Node n) throws IOException, RoutingException { long startTime = System.nanoTime(); Operation op = new ConnectOperation(this.server, this, n, this.config); op.execute(); long endTime = System.nanoTime(); this.statistician.setBootstrapTime(endTime - startTime); } /** * Stores the specified value under the given key * This value is stored on K nodes on the network, or all nodes if there are > K total nodes in the network * * @param content The content to put onto the JSocialKademliaDHT * * @return Integer How many nodes the content was stored on * * @throws java.io.IOException * */ @Override public int put(KadContent content) throws IOException { return this.put(new JSocialKademliaStorageEntry(content)); } /** * Stores the specified value under the given key * This value is stored on K nodes on the network, or all nodes if there are > K total nodes in the network * * @param entry The JSocialKademliaStorageEntry with the content to put onto the JSocialKademliaDHT * * @return Integer How many nodes the content was stored on * * @throws java.io.IOException * */ public int put(JSocialKademliaStorageEntry entry) throws IOException { StoreOperation sop = new StoreOperation(this.server, this, this.compressStorageEntry(entry), this.dht, this.config); sop.execute(); /* Return how many nodes the content was stored on */ return sop.numNodesStoredAt(); } /** * Put the data on the network and also cache a copy locally * * @param content The content to store * * @return How many nodes the content has been stored at excluding the local node. * * @throws java.io.IOException */ public int putAndCache(KadContent content) throws IOException { JSocialKademliaStorageEntry entry = new JSocialKademliaStorageEntry(content); this.cache(entry); return this.put(entry); } /** * Store a content on the local node's JSocialKademliaDHT * * @param content The content to put on the JSocialKademliaDHT * * @throws java.io.IOException */ @Override public void putLocally(KadContent content) throws IOException { this.dht.store(this.compressStorageEntry(new JSocialKademliaStorageEntry(content))); } /** * Stores the specified value under the given key locally; * This content is permanently stored locally and will not be deleted unless the cache is cleared. * * @param content The content to put onto the local JSocialKademliaDHT * * @throws java.io.IOException * */ public void cache(KadContent content) throws IOException { this.cache(new JSocialKademliaStorageEntry(content)); } private void cache(JSocialKademliaStorageEntry entry) throws IOException { this.dht.cache(this.compressStorageEntry(entry)); } /** * Compress the storage entry */ private JSocialKademliaStorageEntry compressStorageEntry(final JSocialKademliaStorageEntry entry) { try { return StorageEntryCompressor.compress(entry); } catch (IOException ex) { System.err.println("Error whiles compressing storage entry. Msg: " + ex.getMessage()); } return entry; } /** * Decompress a given storage entry */ private JSocialKademliaStorageEntry decompressStorageEntry(final JSocialKademliaStorageEntry entry) { try { return StorageEntryCompressor.decompress(entry); } catch (IOException ex) { System.err.println("Error whiles decompressing storage entry. Msg: " + ex.getMessage()); } return entry; } /** * Get some content cached locally on the JSocialKademliaDHT. * * @param param The parameters used to search for the content * * @return DHTContent The content * * @throws java.io.IOException */ public SocialKademliaStorageEntry getCachedContent(GetParameter param) throws NoSuchElementException, IOException { return this.decompressStorageEntry(this.dht.get(param)); } /** * Method called to do an updated of a content in the local storage; this method updates both cached and un-cached content. * * @param param The parameters of the content to update * * @return JSocialKademliaStorageEntry with the updated content * * @throws java.io.IOException * @throws socialkademlia.exceptions.UpToDateContentException */ public SocialKademliaStorageEntry updateContentLocally(GetParameterFUC param) throws IOException, UpToDateContentException, NoSuchElementException { if (this.dht.contains(param)) { return this.getUpdated(param); } else { throw new NoSuchElementException("KademliaNode.updateContentLocally(): This content is not a part of the DHT. "); } } /** * Get some content stored on the JSocialKademliaDHT * * @param param The parameters used to search for the content * * @return DHTContent The content * * @throws java.io.IOException * @throws kademlia.exceptions.ContentNotFoundException */ @Override public JSocialKademliaStorageEntry get(GetParameter param) throws NoSuchElementException, IOException, ContentNotFoundException { if (this.dht.contains(param)) { /* The content is on our JSocialKademliaDHT */ JSocialKademliaStorageEntry e = this.dht.get(param); if (e.getContentMetadata().isCached()) { /** * If it's cached, we check for an updated version * * @note Here we don't log the statistic because the getUpdated() will log for us */ GetParameterFUC gpf = new GetParameterFUC(e.getContentMetadata()); try { /* Get and return an updated version of the content */ return this.decompressStorageEntry(this.getUpdated(gpf)); } catch (UpToDateContentException ex) { /* well the version we have is the latest, lets just return that */ return this.decompressStorageEntry(e); } } else if (e.getContentMetadata().isKNode()) { /* We're one of the k-nodes, lets just return the content */ return this.decompressStorageEntry(e); } else { /* If it's not cached, we just return it since our node is one of the K-Closest */ return this.decompressStorageEntry(e); } } /* Seems like it doesn't exist in our JSocialKademliaDHT, get it from other Nodes */ long startTime = System.nanoTime(); ContentLookupOperation clo = new ContentLookupOperation(server, this, param, this.config); clo.execute(); long endTime = System.nanoTime(); this.statistician.addContentLookup(endTime - startTime, clo.routeLength(), clo.isContentFound()); return this.decompressStorageEntry(clo.getContentFound()); } /** * Get a content and cache it. * * @param gp * * @return The JSocialKademliaStorageEntry with the content * * @throws java.io.IOException * @throws kademlia.exceptions.ContentNotFoundException */ public JSocialKademliaStorageEntry getAndCache(final GetParameter gp) throws IOException, ContentNotFoundException { JSocialKademliaStorageEntry e = this.get(gp); this.cache(e); /** * We have to decompress it again before returning it because the cache method would've compressed it * * @todo decide whether it's better to decompress twice or to copy the storageentry and use one copy for cache() */ return this.decompressStorageEntry(e); } /** * Get some content stored on the JSocialKademliaDHT if there is a newer version than our current version. * * @param param The parameters used to search for the content * * @return JSocialKademliaStorageEntry The content * * @throws java.io.IOException * @throws socialkademlia.exceptions.UpToDateContentException */ public JSocialKademliaStorageEntry getUpdated(GetParameterFUC param) throws IOException, UpToDateContentException { /* We assume the owner always have the latest content, so no need to contact any other node for updated content */ if (param.getOwnerId().equals(this.getOwnerId())) { throw new UpToDateContentException("You are the owner of this content, no need to check other nodes!!!"); } /* Seems like it doesn't exist in our JSocialKademliaDHT, get it from other Nodes */ long startTime = System.nanoTime(); ContentLookupOperationFUC clo = new ContentLookupOperationFUC(server, this, param, this.config); clo.execute(); long endTime = System.nanoTime(); this.statistician.addContentLookupFUC(endTime - startTime, clo.routeLength(), clo.newerContentExist(), clo.isContentFound()); JSocialKademliaStorageEntry latest = clo.getContentFound(); /* If we have this content locally, lets update it too */ try { this.dht.update(latest); } catch (NoSuchElementException ex) { /* Any exception here will be if we don't have the content... just ignore it */ } return this.decompressStorageEntry(latest); } /** * Allow the user of the System to call refresh even out of the normal Kad refresh timing * * @throws java.io.IOException */ @Override public void refresh() throws IOException { new KadRefreshOperation(this.server, this, this.dht, this.config).execute(); } /** * @return String The ID of the owner of this local network */ @Override public String getOwnerId() { return this.ownerId; } /** * @return Integer The port on which this kad instance is running */ @Override public int getPort() { return this.udpPort; } /** * Here we handle properly shutting down the Kademlia instance * * @param saveState Whether to save the application state or not * * @throws java.io.FileNotFoundException */ @Override public void shutdown(final boolean saveState) throws IOException { /* Shut down the server */ this.server.shutdown(); this.stopRefreshOperation(); /* Save this Kademlia instance's state if required */ if (saveState) { /* Save the system state */ this.saveKadState(); } } /** * Saves the node state to a text file * * @throws java.io.FileNotFoundException */ @Override public void saveKadState() throws IOException { DataOutputStream dout; /** * @section Store Basic Kad data */ dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "kad.kns")); new JsonSerializer<JSocialKademliaNode>().write(this, dout); /** * @section Save the node state */ dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "node.kns")); new JsonSerializer<Node>().write(this.localNode, dout); /** * @section Save the routing table * We need to save the routing table separate from the node since the routing table will contain the node and the node will contain the routing table * This will cause a serialization recursion, and in turn a Stack Overflow */ dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "routingtable.kns")); new JsonSocialKadRoutingTableSerializer(this.config).write(this.getRoutingTable(), dout); /** * @section Save the DHT */ dout = new DataOutputStream(new FileOutputStream(getStateStorageFolderName(this.ownerId, this.config) + File.separator + "dht.kns")); new JsonSocialKademliaDHTSerializer().write(this.dht, dout); } /** * Get the name of the folder for which a content should be stored * * @return String The name of the folder to store node states */ private static String getStateStorageFolderName(String ownerId, KadConfiguration iconfig) { /* Setup the nodes storage folder if it doesn't exist */ String path = iconfig.getNodeDataFolder(ownerId) + File.separator + "nodeState"; File nodeStateFolder = new File(path); if (!nodeStateFolder.isDirectory()) { nodeStateFolder.mkdir(); } return nodeStateFolder.toString(); } @Override public SocialKademliaRoutingTable getRoutingTable() { return this.routingTable; } @Override public SocialKadStatistician getStatistician() { return this.statistician; } /** * Creates a string containing all data about this Kademlia instance * * @return The string representation of this Kad instance */ @Override public String toString() { StringBuilder sb = new StringBuilder("\n\nPrinting Kad State for instance with owner: "); sb.append(this.ownerId); sb.append("\n\n"); sb.append("\n"); sb.append("Local Node"); sb.append(this.localNode); sb.append("\n"); sb.append("\n"); sb.append("Routing Table: "); sb.append(this.getRoutingTable()); sb.append("\n"); sb.append("\n"); sb.append("DHT: "); sb.append(this.dht); sb.append("\n"); sb.append("\n\n\n"); return sb.toString(); } @Override public int put(KademliaStorageEntry entry) throws IOException { throw new UnsupportedOperationException("Not supported."); //To change body of generated methods, choose Tools | Templates. } }