/*
* The MIT License
*
* Copyright 2014 sorrge.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.nyan.dch.node;
import com.google.common.collect.Collections2;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.nyan.dch.communication.ProtocolException;
import org.nyan.dch.crypto.SHA256Hash;
import org.nyan.dch.posts.Board;
import org.nyan.dch.posts.IPostAddedListener;
import org.nyan.dch.posts.Post;
import org.nyan.dch.posts.PostData;
import org.nyan.dch.posts.Storage;
import org.nyan.dch.posts.Thread;
/**
* @author sorrge
*/
public class Node implements IRemoteResponseListener, IPostAddedListener
{
private final HashSet<IRemoteNode> connections = new HashSet<>();
private final SetMultimap<IRemoteNode, SHA256Hash> knownPostsByNode = Multimaps.synchronizedSetMultimap(HashMultimap.<IRemoteNode, SHA256Hash>create());
private final HashSet<Post> knownPosts = new HashSet<>();
public final Storage storage;
public Node(Storage storage)
{
this.storage = storage;
storage.AddPostAddedListener(this);
}
@Override
public void OnlineStatusChanged(IRemoteNode node, boolean nodeIsOnline, boolean iAmInitiator)
{
if(nodeIsOnline)
{
connections.add(node);
if(!iAmInitiator)
for(String board : storage.GetChan().GetBoards())
try
{
SortedSet<Thread> myThreads = storage.GetChan().GetBoard(board).GetThreadsChronologically();
ArrayList<SHA256Hash> myThreadIds = new ArrayList<>(myThreads.size());
for(Thread t : myThreads)
myThreadIds.add(t.GetId());
node.UpdateThreadsAndGetOthers(board, myThreadIds);
}
catch(ProtocolException ex)
{
System.err.format("Error updating threads on board %s from node %s: %s", board, node.toString(), ex.getMessage());
}
}
else
{
connections.remove(node);
knownPostsByNode.removeAll(node);
}
}
@Override
public void PostReceived(IRemoteNode node, PostData post)
{
//assert(connections.contains(node));
if(!CheckPost(post))
return;
Post newPost = new Post(post);
knownPostsByNode.put(node, newPost.GetId());
AddPost(newPost, node);
}
private void AddPost(Post newPost, IRemoteNode node)
{
if(knownPosts.add(newPost) && !storage.Contains(newPost))
{
//System.out.printf("Added post %s to node %s\n", newPost.GetId().toString(), toString());
storage.Add(newPost);
Distribute(newPost, node);
}
}
private void Distribute(Post newPost, IRemoteNode excludedNode)
{
for(IRemoteNode otherNode : connections)
if(otherNode != excludedNode && knownPostsByNode.put(otherNode, newPost.GetId()))
PushPostToNode(otherNode, newPost);
}
private void PushPostToNode(IRemoteNode otherNode, Post newPost)
{
try
{
otherNode.PushPost(newPost.GetData());
}
catch(ProtocolException pe)
{
System.err.format("Error sending post %s to node %s: %s", newPost.GetId().toString(), otherNode.toString(), pe.getMessage());
}
}
@Override
public void UpdateThreadsAndSendOthers(IRemoteNode node, String board, Collection<SHA256Hash> otherThreads)
{
knownPostsByNode.putAll(node, otherThreads);
Board b = storage.GetChan().GetBoard(board);
SortedSet<Thread> myThreads = b.GetThreadsChronologically();
//System.out.printf("Node %s requested %d threads to update from node %s\n", node.toString(), otherThreads.size(), toString());
for(Thread thread : myThreads)
{
if(otherThreads.contains(thread.GetId()))
GetOtherPosts(node, board, thread.GetId(), thread.GetPostIds());
else
for(Post p : thread.GetPosts())
PushPostToNode(node, p);
}
for(SHA256Hash threadId : otherThreads)
if(!b.GetThreads().contains(threadId))
GetOtherPosts(node, board, threadId, new ArrayList<>()); // request the entire thread
}
private void GetOtherPosts(IRemoteNode node, String board, SHA256Hash threadId, Collection<SHA256Hash> posts)
{
try
{
node.GetOtherPostsInThread(board, threadId, posts);
}
catch(ProtocolException ex)
{
System.err.format("Error synchronizing thread %s from boards %s with node %s: %s", threadId.toString(), board, node.toString(), ex.getMessage());
}
}
@Override
public void SendOtherPostsInThread(IRemoteNode node, String board, SHA256Hash threadId, Collection<SHA256Hash> otherPosts)
{
knownPostsByNode.putAll(node, otherPosts);
Thread t = storage.GetChan().GetBoard(board).GetThread(threadId);
if(t == null)
{
//System.out.printf("Requested thread %s not found on node %s\n", threadId.toString(), toString());
return;
}
//System.out.printf("Syncing thread %s from node %s with node %s\n", threadId.toString(), toString(), node.toString());
Set<SHA256Hash> otherSet = new HashSet<>(otherPosts);
for(Post p : t.GetPosts())
if(!otherSet.contains(p.GetId()))
PushPostToNode(node, p);
Set<SHA256Hash> toPull = Sets.difference(otherSet, t.GetPostIds());
if(!toPull.isEmpty())
try
{
node.PullPosts(toPull);
}
catch (ProtocolException ex)
{
System.err.printf("Error requesting %d posts from node %s: %s\n", toPull.size(), node.toString(), ex.getMessage());
}
}
@Override
public void SendPosts(IRemoteNode node, Collection<SHA256Hash> postsNeeded)
{
for(SHA256Hash id : postsNeeded)
if(storage.Contains(id))
{
PushPostToNode(node, storage.GetPost(id));
knownPostsByNode.put(node, id);
}
}
public void AddLocalPost(Post post)
{
AddPost(post, null);
}
public Collection<IRemoteNode> Connections()
{
return connections;
}
@Override
public void PostAdded(Post post)
{
Distribute(post, null);
}
private boolean CheckPost(PostData post)
{
String body = post.GetBody();
if(body.length() == 0 || body.length() > 500)
return false;
if(post.GetSentAt().getTime() - new Date().getTime() > 3 * 60 * 1000)
return false;
if(body.codePoints().anyMatch(cp -> Character.isISOControl(cp)))
return false;
return true;
}
}