
/* Author: Josh Holtrop
 * GVSU, CS654, Project 1
 * Date: 2008-03-11
 */

import java.io.*;
import java.net.*;
import java.util.*;

/* KaZaServer implements a "Mini Napster Hub" group leader peer
 * for the KaZa file sharing system. The listening server runs
 * as a thread, and each incoming connection to it is also handled
 * by a thread so that multple clients can be connected and upload
 * their file list and descriptions as well as search for files
 * simultaneously.
 */
public class KaZaServer implements Runnable
{
    public static final int LISTEN_PORT = 3442;
    private ServerSocket m_serverSocket;
    private HashMap<String, ClientInfo> m_clientData;
    private Vector<Socket> m_peerGroupLeaders;
    private HashMap<String, Integer> m_currentSearches;

    /* create a KaZaServer */
    public KaZaServer()
    {
        m_clientData = new HashMap<String, ClientInfo>();
        m_peerGroupLeaders = new Vector<Socket>();
        m_currentSearches = new HashMap<String, Integer>();
    }

    /* begin listening for incoming connections */
    public void run()
    {
        try {
            m_serverSocket = new ServerSocket(LISTEN_PORT);

            /* porcess connection requests */
            for (;;)
            {
                Socket clientSocket = m_serverSocket.accept();

                Thread thread = new Thread(new ClientHandler(clientSocket));
                thread.start();
            }
        } catch (IOException ioe) {
            System.out.println("KaZaServer: IO Exception!");
            ioe.printStackTrace();
            return;
        }
    }

    /* the MNH server administrator invokes this method to instruct
     * the server to connect to a peer group leader. This peer group
     * leader is then used to forward searches to aggregate search
     * results for peers connected to linked-to group leaders */
    public void connectToPeerGroupLeader(String peerName,
                                         boolean sendReverseConnectMsg)
    {
        /* check if the peer group leader is already in our list */
        synchronized (m_peerGroupLeaders)
        {
            for (Socket s : m_peerGroupLeaders)
            {
                if (s.getInetAddress().getHostAddress().equals(peerName))
                    return;
            }
        }

        Socket s;
        try
        {
            s = new Socket(peerName, LISTEN_PORT);
            DataOutputStream os = new DataOutputStream(s.getOutputStream());
            os.writeBytes("HELO KaZaServer\n");
            if (sendReverseConnectMsg)
                os.writeBytes("GRPL\n");
        }
        catch (Exception e)
        {
            return;
        }

        /* add new peer group leader to the peer group leaders list */
        synchronized (m_peerGroupLeaders)
        {
            m_peerGroupLeaders.add(s);
        }
    }

    public void close()
    {
    }

    /* return a list of the currently connected clients */
    public Object[] getClientList()
    {
        Set<String> s;
        synchronized (m_clientData)
        {
            s = m_clientData.keySet();
        }
        return s.toArray();
    }

    /* return a list of the linked-to peer group leaders */
    public Object[] getPeerGroupLeaderList()
    {
        Vector<String> peers = new Vector<String>();
        synchronized (m_peerGroupLeaders)
        {
            for (Socket s : m_peerGroupLeaders)
            {
                peers.add(s.getInetAddress().getHostAddress());
            }
        }
        return peers.toArray();
    }

    /* simple class to store information about a connected KaZaClient */
    private class ClientInfo
    {
        String userName = "Anonymous";
        int speed = 1500;
        HashMap<String, String> files = new HashMap<String, String>();
    }

    /* this class can be run in a thread and handles communication
     * with a single client */
    private class ClientHandler implements Runnable
    {
        private Socket m_socket;
        private String m_clientIP;
        ClientInfo m_clientInfo;

        public ClientHandler(Socket socket)
        {
            m_socket = socket;
            m_clientIP = m_socket.getInetAddress().getHostAddress();
            synchronized (m_clientData)
            {
                if (!m_clientData.containsKey(m_clientIP))
                { 
                    m_clientData.put(socket.getInetAddress().getHostAddress(),
                            new ClientInfo());
                }
                m_clientInfo = m_clientData.get(m_clientIP);
            }
        }

        public void run()
        {
            try
            {
                BufferedReader br = new BufferedReader(
                                        new InputStreamReader(
                                            m_socket.getInputStream()));
                DataOutputStream os = new DataOutputStream(
                                          m_socket.getOutputStream());

                /* loop processing client messages */
                boolean running = true;
                while (running)
                {
                    String inLine = br.readLine();

                    StringTokenizer tokens = new StringTokenizer(inLine);
                    if (!tokens.hasMoreTokens())
                        continue;

                    String opCode = tokens.nextToken();
                    opCode = opCode.toUpperCase();
                    if (opCode.equals("HELO"))
                    {
                        /* user is announcing his or her username */
                        synchronized (m_clientData)
                        {
                            m_clientInfo.userName = inLine.substring(5);
                        }
//                        System.out.println("Got HELO from " + m_clientInfo.userName);
                    }
                    else if (opCode.equals("SPED"))
                    {
                        /* user is telling us their connection speed */
                        if (tokens.hasMoreTokens())
                        {
                            int speed = Integer.parseInt(tokens.nextToken());
                            synchronized (m_clientData)
                            {
                                m_clientInfo.speed = speed;
                            }
//                            System.out.println("Got SPED of " + m_clientInfo.speed);
                        }
                    }
                    else if (opCode.equals("DESC"))
                    {
                        /* user is giving us a description of a file */
                        String fileName = inLine.substring(5);
                        String fileDesc = br.readLine();
                        synchronized (m_clientData)
                        {
                            m_clientInfo.files.put(fileName, fileDesc);
                        }
//                        System.out.println("Got DESC of '" + fileName +
//                                           "': '" + fileDesc + "'");
                    }
                    else if (opCode.equals("SRCH"))
                    {
                        /* user is requesting a search */
                        if (tokens.hasMoreTokens())
                        {
                            int depth = Integer.parseInt(tokens.nextToken());
                            if (tokens.hasMoreTokens())
                            {
                                String query = tokens.nextToken();
                                String searchResults = performSearch(depth, query, m_socket);
                                os.writeBytes(searchResults);
                                os.writeBytes(".\n");
//                                System.out.println("Got SRCH for " + query +
//                                                   ", results:\n" + searchResults);
                            }
                        }
                    }
                    else if (opCode.equals("QUIT"))
                    {
                        /* user is requesting to leave the system */
                        br.close();
                        os.close();
                        m_socket.close();
                        running = false;
                    }
                    else if (opCode.equals("GRPL"))
                    {
                        /* user is a peer group leader, initiate a connection
                         * to it if one is not established */
                        connectToPeerGroupLeader(m_clientIP, false);
                    }
                }
            }
            catch (Exception e) { }
            finally
            {
                synchronized (m_clientData)
                {
                    m_clientData.remove(m_clientIP);
                }
            }
        }
    }

    /* this method is called when a client is asking our server to
     * perform a keyword search on its file database */
    private String performSearch(int depth, String query, Socket clientSocket)
    {
        String results = "";
        String querylc = query.toLowerCase();

        /* don't perform a search that we are already performing
         * (avoid duplicate results) */
        synchronized (m_currentSearches)
        {
            if (m_currentSearches.containsKey(querylc))
                return results;
            m_currentSearches.put(querylc, 1);
        }

        /* if the depth > 0, then we can forward this search to our
         * peer group leaders to aggregate their results with ours */
        if (depth > 0)
        {
            synchronized (m_peerGroupLeaders)
            {
                for (int i = 0; i < m_peerGroupLeaders.size(); i++)
                {
                    if (m_peerGroupLeaders.elementAt(i).isClosed())
                    {
                        m_peerGroupLeaders.removeElementAt(i);
                        i--;
                    }
                    else
                    {
                        String peerResults =
                            getSearchResultsFromPeerGroupLeader(
                                    m_peerGroupLeaders.elementAt(i),
                                    depth-1, query);
                        results += peerResults;
                    }
                }
            }
        }

        /* now search my own file list */
        synchronized (m_clientData)
        {
            Set<String> clients = m_clientData.keySet();
            for (String clientIP : clients)
            {
                HashMap<String, String> files = m_clientData.get(clientIP).files;
                Set<String> clientFiles = files.keySet();
                for (String fileName : clientFiles)
                {
                    String fnamelc = fileName.toLowerCase();
                    if ( (fnamelc.indexOf(querylc) >= 0) ||
                         (files.get(fileName).toLowerCase().indexOf(querylc) >= 0) )
                    {
                        /* clientIPToReturn will hold our MNH server's opinion
                         * of the client IP address which has the file being
                         * searched for *unless* it starts with "127.", in which
                         * case it is a local connection and the client performing
                         * the search should receive back whichever IP it is using
                         * to talk to the MNH server */
                        String clientIPToReturn =
                            clientIP.startsWith("127.")
                                ? clientSocket.getLocalAddress().getHostAddress()
                                : clientIP;
                        results += clientIPToReturn + "|" +
                                   m_clientData.get(clientIP).userName + "|" +
                                   m_clientData.get(clientIP).speed + "|" +
                                   fileName + "|" +
                                   files.get(fileName) + "\n";
                    }
                }
            }
        }

        synchronized (m_currentSearches)
        {
            m_currentSearches.remove(querylc);
        }

        return results;
    }

    /* helper method invoked to forward keyword search to peer group leader */
    private String getSearchResultsFromPeerGroupLeader(Socket s, int depth,
                                                       String query)
    {
        String searchResults = "";
        try
        {
            BufferedReader br = new BufferedReader(
                                    new InputStreamReader(
                                        s.getInputStream()));
            DataOutputStream os = new DataOutputStream(s.getOutputStream());

            os.writeBytes("SRCH " + depth + " " + query + "\n");
            for (;;)
            {
                String inLine = br.readLine();
                if (inLine.equals("."))
                    break;
                searchResults += inLine + "\n";
            }
        }
        catch (Exception e)
        {
            return "";
        }
        return searchResults;
    }
}
