package socialkademlia.dht;
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.util.List;
import java.util.NoSuchElementException;
import kademlia.KadConfiguration;
import kademlia.dht.GetParameter;
import kademlia.dht.KadContent;
import kademlia.dht.KademliaStorageEntryMetadata;
import kademlia.exceptions.ContentExistException;
import kademlia.exceptions.ContentNotFoundException;
import kademlia.node.KademliaId;
import kademlia.util.serializer.JsonSerializer;
import kademlia.util.serializer.KadSerializer;
/**
* The main Distributed Hash Table class that manages the entire JSocialKademliaDHT
*
* @author Joshua Kissoon
* @since 20140226
*
* @todo Inherit the kademlia.dht.DHT class to remove the excess methods that are replicated
*/
public class JSocialKademliaDHT implements SocialKademliaDHT
{
private transient StoredContentManager contentManager;
private transient KadSerializer<JSocialKademliaStorageEntry> serializer = null;
private transient KadConfiguration config;
private final String ownerId;
public JSocialKademliaDHT(String ownerId, KadConfiguration config)
{
this.ownerId = ownerId;
this.config = config;
this.initialize();
}
/**
* Initialize this JSocialKademliaDHT to it's default state
*/
@Override
public final void initialize()
{
contentManager = new StoredContentManager();
}
/**
* Set a new configuration. Mainly used when we restore the JSocialKademliaDHT state from a file
*
* @param con The new configuration file
*/
@Override
public void setConfiguration(KadConfiguration con)
{
this.config = con;
}
/**
* Creates a new Serializer or returns an existing serializer
*
* @return The new ContentSerializer
*/
@Override
public KadSerializer<JSocialKademliaStorageEntry> getSerializer()
{
if (null == serializer)
{
serializer = new JsonSerializer<>();
}
return serializer;
}
/**
* Handle storing content locally
*
* @param content The JSocialKademliaDHT content to store
*
* @return boolean true if we stored the content, false if the content already exists and is up to date
*
* @throws java.io.IOException
*/
@Override
public boolean store(JSocialKademliaStorageEntry content) throws IOException
{
boolean cached = content.getContentMetadata().isCached(); // Should we cache this content
boolean isKNode = content.getContentMetadata().isKNode(); // Is this node one of the k-node
/* Lets check if we have this content and it's the updated version */
if (this.contentManager.contains(content.getContentMetadata()))
{
SocialKademliaStorageEntryMetadata current = this.contentManager.get(content.getContentMetadata());
/* update the last republished time */
current.updateLastRepublished();
/* We have the current content, no need to update it! */
if (current.getLastUpdatedTimestamp() >= content.getContentMetadata().getLastUpdatedTimestamp())
{
/* Cache it if required */
if (cached)
{
current.setCached();
}
/* Set this is a K-Node if required */
if (isKNode)
{
current.setKNode();
}
return false;
}
else
{
/* We got here means we don't have the current content, lets update it */
/* If the current version is a cached version, remember to cache it back if we need to do an update */
if (current.isCached())
{
cached = true;
}
/* If this is a k-node for the current version, remember to set that back after an update */
if (current.isKNode())
{
isKNode = true;
}
/* Since we don't have the latest version, lets delete it so the new version will be added below */
try
{
this.absoluteRemove(current);
}
catch (ContentNotFoundException ex)
{
/* This won't ever happen at this point since we only get here if the content is found, lets ignore it */
}
}
}
/* We got here means we need to add the content or re-add it to update it */
try
{
/* Store the content to a file and then keep track of this content in the entries manager */
content.getContentMetadata().updateLastRepublished();
content.getContentMetadata().setCached(cached);
content.getContentMetadata().setKNode(isKNode);
this.contentManager.put(content.getContentMetadata());
this.putContentToFile(content, content.getContentMetadata());
return true;
}
catch (ContentExistException e)
{
/**
* Content already exist on the DHT
* This won't happen because above takes care of removing the content if it's older and needs to be updated,
* or returning if we already have the current content version.
*/
return false;
}
}
@Override
public boolean store(KadContent content) throws IOException
{
return this.store(new JSocialKademliaStorageEntry(content));
}
/**
* Handle storing content locally to keep the content cached.
*
* We set that this content is a cached entry and that this node is not one of the k-nodes.
*
* @param content The JSocialKademliaDHT content to store
*
* @return boolean true if we stored the content, false if the content already exists and is up to date
*
* @throws java.io.IOException
*/
@Override
public boolean cache(JSocialKademliaStorageEntry content) throws IOException
{
content.getContentMetadata().setCached();
content.getContentMetadata().setKNode(false);
return this.store(content);
}
@Override
public boolean cache(KadContent content) throws IOException
{
return this.cache(new JSocialKademliaStorageEntry(content));
}
/**
* Write the given storage entry to it's file
*/
private void putContentToFile(JSocialKademliaStorageEntry content, SocialKademliaStorageEntryMetadata entryMD) throws IOException
{
String contentStorageFolder = this.getContentStorageFolderName(content.getContentMetadata().getKey());
try (FileOutputStream fout = new FileOutputStream(contentStorageFolder + File.separator + entryMD.hashCode() + ".kct");
DataOutputStream dout = new DataOutputStream(fout))
{
this.getSerializer().write(content, dout);
}
}
/**
* Update a content; the operation is only done iff we already have a copy of the content here
*
* @param newContent The content to update.
*
* @throws java.io.IOException
*/
@Override
public void update(JSocialKademliaStorageEntry newContent) throws IOException
{
if (this.contentManager.contains(newContent.getContentMetadata()))
{
this.store(newContent);
}
else
{
throw new NoSuchElementException("This content is not on the DHT currently, cannot update it.");
}
}
/**
* Retrieves a Content from local storage
*
* @param key The Key of the content to retrieve
* @param hashCode The hash code of the content to retrieve
*
* @return A KadContent object
*
* @throws java.io.IOException
*/
@Override
public JSocialKademliaStorageEntry retrieve(KademliaId key, int hashCode) throws FileNotFoundException, IOException, ClassNotFoundException
{
String folder = this.getContentStorageFolderName(key);
DataInputStream din = new DataInputStream(new FileInputStream(folder + File.separator + hashCode + ".kct"));
return this.getSerializer().read(din);
}
/**
* Check if any content for the given criteria exists in this JSocialKademliaDHT
*
* @param param The content search criteria
*
* @return boolean Whether any content exist that satisfy the criteria
*/
@Override
public boolean contains(GetParameter param)
{
return this.contentManager.contains(param);
}
/**
* Retrieve and create a KadContent object given the JSocialKademliaStorageEntry object
*
* @param entry The JSocialKademliaStorageEntry used to retrieve this content
*
* @return KadContent The content object
*
* @throws java.io.IOException
*/
@Override
public JSocialKademliaStorageEntry get(SocialKademliaStorageEntryMetadata entry) throws IOException, NoSuchElementException
{
try
{
return this.retrieve(entry.getKey(), entry.hashCode());
}
catch (FileNotFoundException e)
{
System.err.println("Error while loading file for content. Message: " + e.getMessage());
}
catch (ClassNotFoundException e)
{
System.err.println("The class for some content was not found. Message: " + e.getMessage());
}
/* If we got here, means we got no entries */
throw new NoSuchElementException();
}
/**
* Get the JSocialKademliaStorageEntry for the content if any exist,
* retrieve the KadContent from the storage system and return it
*
* @param param The parameters used to filter the content needed
*
* @return KadContent A KadContent found on the JSocialKademliaDHT satisfying the given criteria
*
* @throws java.io.IOException
*/
@Override
public JSocialKademliaStorageEntry get(GetParameter param) throws NoSuchElementException, IOException
{
/* Load a KadContent if any exist for the given criteria */
try
{
SocialKademliaStorageEntryMetadata e = this.contentManager.get(param);
return this.retrieve(e.getKey(), e.hashCode());
}
catch (FileNotFoundException e)
{
System.err.println("Error while loading file for content. Message: " + e.getMessage());
}
catch (ClassNotFoundException e)
{
System.err.println("The class for some content was not found. Message: " + e.getMessage());
}
/* If we got here, means we got no entries */
throw new NoSuchElementException();
}
/**
* Delete a content from local storage
*
* @param content The Content to Remove
*
*
* @throws kademlia.exceptions.ContentNotFoundException
*/
@Override
public void remove(KadContent content) throws ContentNotFoundException
{
this.remove(new JSocialKademliaStorageEntryMetadata(content));
}
/**
* Similar to the remove method, however, in this case, we remove the content even if it's cached
*/
private void absoluteRemove(SocialKademliaStorageEntryMetadata entry) throws ContentNotFoundException
{
contentManager.remove(entry);
String folder = this.getContentStorageFolderName(entry.getKey());
File file = new File(folder + File.separator + entry.hashCode() + ".kct");
if (file.exists())
{
file.delete();
}
else
{
throw new ContentNotFoundException();
}
}
@Override
public void remove(SocialKademliaStorageEntryMetadata entry) throws ContentNotFoundException
{
/* If it's cached data, we don't remove it, just set that we are no longer one of the k-closest */
if (this.contentManager.get(entry).isCached())
{
this.contentManager.get(entry).setKNode(false);
return;
}
this.absoluteRemove(entry);
}
/**
* Get the name of the folder for which a content should be stored
*
* @param key The key of the content
*
* @return String The name of the folder
*/
private String getContentStorageFolderName(KademliaId key)
{
/**
* Each content is stored in a folder named after the first 2 characters of the NodeId
*
* The name of the file containing the content is the hash of this content
*/
String folderName = key.hexRepresentation().substring(0, 2);
File contentStorageFolder = new File(this.config.getNodeDataFolder(ownerId) + File.separator + folderName);
/* Create the content folder if it doesn't exist */
if (!contentStorageFolder.isDirectory())
{
contentStorageFolder.mkdir();
}
return contentStorageFolder.toString();
}
/**
* @return A List of all StorageEntries for this node
*/
@Override
public List<SocialKademliaStorageEntryMetadata> getStorageEntries()
{
return contentManager.getAllEntries();
}
/**
* @return A List of all StorageEntries of cached content for this node
*/
public List<SocialKademliaStorageEntryMetadata> getCachedStorageEntries()
{
return contentManager.getAllCachedEntries();
}
/**
* Used to add a list of storage entries for existing content to the JSocialKademliaDHT.
* Mainly used when retrieving StorageEntries from a saved state file.
*
* @param ientries The entries to add
*/
@Override
public void putStorageEntries(List<SocialKademliaStorageEntryMetadata> ientries)
{
for (KademliaStorageEntryMetadata e : ientries)
{
SocialKademliaStorageEntryMetadata se = (SocialKademliaStorageEntryMetadata) e;
try
{
this.contentManager.put(se);
}
catch (ContentExistException ex)
{
/* Entry already exist, no need to store it again */
}
}
}
@Override
public synchronized String toString()
{
return this.contentManager.toString();
}
}