/*
	David Tate

	java BeaconClient name key numServers (serverAddress serverPort)^numServers 
	
	A simple client that talks to its central server using
		our protocol to see other users in the area.
	The basic protocol is that it sends messages of type
		BeaconMessage, which can have various types.

	The client logs in and recieves a dump of all the
		users online right now. It then sits and listens
		for messages from the server about users entering
		and leaving the area.
	The client starts a simple shell and recieves commands
		from the user.  These are translated to BeaconMessages
		and sent to other BeaconClients. The thread 
		BeaconShell does this.
	Therefore this client also acts as a simple server that 
		responds to BeaconMessages. The thread MessageHandler 
		does this and also listens for messages from the central
		server.
	
	This is the command line version only. This client could
		be embedded into other applications since it has no GUI.
*/

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

public class BeaconClient {
	private String name;				//our username
	private String key;				//our password
	private InetAddress address;	
	private int port;					
	private String[] servAddress;	//server address and port
	private String[] servPort;
	private int numServers;

	private Token curToken = null;	//our system authorization token

	//key server info
	private String keyAddress = null;
	private int keyPort = -1;

	//constants for initial sizes
	private final int INIT_USERLIST_NUM = 50;
	private final int INIT_SESSION_NUM = 50;
	private final int	INIT_NUM_FILES = 10;

	//client variables
	private Vector users;		//all users online right now
	private HashMap userMap;	//a hashtable to store tokens with users

	//server variables
	private ServerSocket server;	//socket where we listen
	private Vector servingTokens;	//clients we are serving now
	private Vector fileList;		//files we are offering 
	
	BeaconClient (String name, String key, String[] servAddress, String[] servPort) {
		this.name = name;
		this.key = key;
		this.servAddress = servAddress;
		this.servPort = servPort;
		this.numServers = servPort.length;

		//online users + their tokens
		users = new Vector (INIT_USERLIST_NUM);	
		userMap = new HashMap();						

		//Setup our files that we are serving
		fileList = new Vector (INIT_NUM_FILES);
		buildFileList();

		//list of tokens that we are serving (session ids)
		servingTokens = new Vector (INIT_SESSION_NUM);

		try {
			server = new ServerSocket (0);		

			//get our state info to send to server
			this.port = server.getLocalPort();	
			this.address = InetAddress.getLocalHost();

			login();	//login to server and get all online users

			//shell that user sees, has higher priority for faster response time
			BeaconShell sh = new BeaconShell();
			sh.setPriority (Thread.NORM_PRIORITY + 1);
			sh.start();
		
			//listen for new connections from server or from other clients.
			try {
				Thread t;
				while (true) {
					Socket s = server.accept();
					ObjectOutputStream out = new ObjectOutputStream (s.getOutputStream());
					out.flush();
					ObjectInputStream in = new ObjectInputStream (s.getInputStream());

					t = new MessageHandler(s, in, out);
					t.start();
				}
			}
			catch (BindException be) {
				System.out.println(be);
				be.printStackTrace();
			}
			catch (IOException ioe) {
				System.out.println(ioe);
				ioe.printStackTrace();
			}
			finally {
				try {
					if (server != null) server.close();	
				}
				catch (IOException ie) { /* already closed */}

			}
		}
		catch (SocketException se) {
			System.out.println (se);
			se.printStackTrace();
			System.out.println ("Couldn't open a listener to recieve messages from the server so there is no use in continuing...");
			System.exit(1);
		}
		catch (IOException ioe) {
			System.out.println (ioe);
			ioe.printStackTrace();
			System.out.println ("Couldn't open a listener to recieve messages from the server so there is no use in continuing...");
			System.exit(1);
		}
	}

	private void buildFileList () {
		//get files that we are serving from the current directory
		File cwd = new File (System.getProperty ("user.dir"));

		//count the number of files we are serving now
		int count;
		count = fileList.size();

		File[] files = cwd.listFiles(new FilenameFilter() {
			public boolean accept (File directory, String name) {
				if (name.endsWith(".java") || name.endsWith(".class")) 
					return false;
				else 
					return true;
			}
		});

		//rebuild fileList from this if needed
		//we rebuild only if there has been a new 
		//file added to the directory since last time
		if (files.length > count) {	
			fileList.removeAllElements();
			for (int i = 0; i < files.length; i++) {
				fileList.addElement (files[i]);
			}
		}
	}
	
	/* key management routines.  Just send message to BeaconKeyServer and interpret response. */
	private void revoke(Token tok) {
		try {
			Socket revokeConn = new Socket(keyAddress, keyPort);
			ObjectOutputStream out = new ObjectOutputStream(revokeConn.getOutputStream());
         out.flush();
         ObjectInputStream in = new ObjectInputStream(revokeConn.getInputStream());
	
			Object[] args = new Object[1];
			args[0] = tok;
			out.writeObject(new BeaconMessage(
				BeaconMessage.REVOKE, 
				args, 
				null, 
				null, 
				0));
		}
      catch (SocketException se) {
      	System.out.println (se);
      }
		catch (IOException ie) {
      	System.out.println(ie);
         ie.printStackTrace();
      }
	}

	private Token authenticate(String name, String key) {
		try {
			Socket authConn = new Socket(keyAddress, keyPort);
			ObjectOutputStream out = new ObjectOutputStream(authConn.getOutputStream());
         out.flush();
         ObjectInputStream in = new ObjectInputStream(authConn.getInputStream());

			//send credentials
			Object[] args = new Object[2];
			args[0] = name;
			args[1] = key;
         out.writeObject(new BeaconMessage(
				BeaconMessage.AUTHENTICATE, 
				args, 
				null, 
				null, 
				0));
			
			//get response
         Object o = in.readObject();
         BeaconMessage m = (BeaconMessage) o;

			if (m.getType() == BeaconMessage.INVALID_USER) {
				return null;
			}
			else if (m.getType() == BeaconMessage.AUTHENTICATE_RESPONSE) {
				Token tok = (Token) m.getArg(0);
				return tok;
			}
		}
		catch (OptionalDataException ode) {
      	System.out.println(ode);
      }
      catch (ClassNotFoundException cnfe) {
      	System.out.println(cnfe);
      }
      catch (SocketException se) {
      	System.out.println(se);
      }
		catch (IOException ie) {
      	System.out.println(ie);
         ie.printStackTrace();
      }
		return null;
	}

	private boolean validate(Token token, String name) {
		if (token == null)
			return false;

		try {
			Socket validConn = new Socket(keyAddress, keyPort);
			ObjectOutputStream out = new ObjectOutputStream(validConn.getOutputStream());
         out.flush();
         ObjectInputStream in = new ObjectInputStream(validConn.getInputStream());

			Object[] args = new Object[1];
			args[0] = token;
         out.writeObject(new BeaconMessage(
				BeaconMessage.VALIDATE, 
				args, 
				null, 
				null, 
				0));
	
         out.flush();

			//get response
         Object o = in.readObject();
         BeaconMessage m = (BeaconMessage) o;

			if (m.getType() == BeaconMessage.INVALID_TOKEN) {
				return false;
			}
			else if (m.getType() == BeaconMessage.TOKEN_EXPIRED) {
				return false;	
			}
			else if (m.getType() == BeaconMessage.VALIDATE_RESPONSE) {
				String username = (String) m.getArg(0);
				return username.equals(name);
			}
		}
		catch (OptionalDataException ode) {
      	System.out.println(ode);
      }
      catch (ClassNotFoundException cnfe) {
      	System.out.println(cnfe);
      }
      catch (SocketException se) {
      	System.out.println (se);
      }
		catch (IOException ie) {
      	System.out.println(ie);
         ie.printStackTrace();
      }
		return false;
	}

	private void login() {
		//login to server 
		System.out.println("" + '\n');
		System.out.println("Logging in......" + '\n');
		
		//setup piping for streams
		for (int i = 0; i < numServers; i++) {
			try {
				Socket loginConnection = new Socket(servAddress[i], Integer.parseInt(servPort[i]));
				ObjectOutputStream out = new ObjectOutputStream(loginConnection.getOutputStream());
				out.flush();
				ObjectInputStream in = new ObjectInputStream(loginConnection.getInputStream());
				out.writeObject(new BeaconMessage(
					BeaconMessage.LOGIN, 
					this.name, 
					this.address, 
					this.port));

				out.flush();

				//recieve login messages		
				while (true) {
					Object o = in.readObject();
					BeaconMessage msg = (BeaconMessage) o;

					if (msg.getType() == BeaconMessage.END_LOGIN) {
						break;
					}
					else if (msg.getType() == BeaconMessage.KEYSERVER_INFO) {
						keyAddress = (String) msg.getArg(0);
						keyPort = ((Integer) msg.getArg(1)).intValue();
					}
					else {	//USER msg, so add to our list
						users.addElement(new BeaconState(msg.getName(), msg.getAddress(), msg.getPort()));
					}
				}  
				System.out.println("You are logged on as user " + name + " on port: " + port + " on server " + servAddress[i] + " on port: " + servPort[i] + '\n');	
			}
			catch (OptionalDataException ode) {
				System.out.println(ode);
				System.out.println ("Failed to login, exiting...");	
				System.exit (1);
			}
			catch (ClassNotFoundException cnfe) {
				System.out.println(cnfe);
				System.out.println ("Failed to login, exiting...");	
				System.exit (1);
			}
			catch (SocketException se) {
				System.out.println (se);
				System.out.println ("Failed to login, exiting...");	
				System.exit (1);
			}
			catch (IOException ioe) {
				System.out.println (ioe);
				ioe.printStackTrace();
				System.out.println ("Failed to login, exiting...");	
				System.exit (1);
			}
		}//end for
		if (keyAddress == null || keyPort == -1) {
			System.out.println("Failed to connect to a keyserver, so exiting...");
			System.exit(1);

		}
		else
			System.out.println("You are using key server: " + keyAddress + " " + keyPort);
			
	}
	
	private void logout() {
		//logout from server

		System.out.println("Logging out...." + '\n');

		for (int i = 0; i < numServers; i++) {
      	try {
				Socket logoutConnection = new Socket(servAddress[i], Integer.parseInt(servPort[i]));
				ObjectOutputStream out = new ObjectOutputStream(logoutConnection.getOutputStream());
				out.flush();
				ObjectInputStream in = new ObjectInputStream(logoutConnection.getInputStream());

         	BeaconMessage logout = new BeaconMessage(BeaconMessage.LOGOUT, name, address, port);
         	out.writeObject(logout);
				out.flush();
      	}
      	catch (ConnectException ce) {
				System.out.println("Failed to connect to server to logout; server may have crashed");
			}
      	catch (IOException ioe) {
         	System.out.println(ioe);
				System.out.println("Failed to connect to server to logout; server may have crashed");
      	}
		} //end for
	}
	
	/* 
		For each message from central server (LOGIN, LOGOUT, USER) or 
		request from another BeaconClient we create one of these threads
		to handle it, so here we are acting as a server.
	*/

	class MessageHandler extends Thread {
		private ObjectOutputStream out;
		private ObjectInputStream in;
		private Socket conn;

		MessageHandler(Socket s, ObjectInputStream i, ObjectOutputStream o) {
			this.conn = s;
			this.out = o;
			this.in = i; 
		}

		public void run() {
			try {

				//retrieve message
				Object oMsg = in.readObject();
				BeaconMessage msg = (BeaconMessage) oMsg;
				int type = msg.getType();
		
				switch(type) {
					case BeaconMessage.LOGIN:
						users.addElement(new BeaconState(msg.getName(), msg.getAddress(), msg.getPort()));
						break;
					case BeaconMessage.LOGOUT:
						users.removeElement(new BeaconState(msg.getName(), msg.getAddress(), msg.getPort()));
						break;
					case BeaconMessage.USER:	//we shouldn't recieve this message after we have logged in, but just in case
						users.addElement(new BeaconState(msg.getName(), msg.getAddress(), msg.getPort()));
						break;
					case BeaconMessage.LIST:
						handleList(msg, out, in);
						break;
					case BeaconMessage.PUT:
						handlePut(msg, out, in);
						break;
					case BeaconMessage.GET:
						handleGet(msg, out, in);
						break;
					case BeaconMessage.SEARCHGET:
						handleSearchGet(msg, out, in);
						break;
					default:
						System.out.println("Invalid message recieved from central server or client:" + msg + '\n');
				}
			}
			/* 
				The following errors are thrown from 
				a single message doing something wrong, 
				so we just print out a message and 
				keep listening; we don't want to 
				close because of one bad I/O op.
			*/
			catch (OptionalDataException ode) {
				System.out.println(ode);
				ode.printStackTrace();
			}
			catch (ClassNotFoundException cnfe) {
				System.out.println(cnfe);
				cnfe.printStackTrace();
			}
			catch (SocketException se) {
				System.out.println(se);
				se.printStackTrace();
			}
			catch (IOException ioe) {
				System.out.println(ioe);
				ioe.printStackTrace();
			}
			finally {
				try {
					if (conn != null)  conn.close();
				}
				catch (IOException ie) {/* already closed */ }
			}
		} //end run

		private boolean validateFilename(String filename) {
			//make sure the filename is one that we want to be serving

			synchronized (fileList) {
				Iterator iter = fileList.iterator();
				while (iter.hasNext()) {
					File tmpFile = (File)iter.next();
					if (tmpFile.getName().equals(filename) && !tmpFile.isDirectory() ) {
						return true;
					}
				}	
			}
			return false;
		}

		private void handleList(BeaconMessage msg, ObjectOutputStream out, ObjectInputStream in) {
			buildFileList();	

			try {
				BeaconState user = new BeaconState(msg.getName(), msg.getAddress(), msg.getPort());
				Token token = (Token)(msg.getArg(0));		
				
				if (validate (token, msg.getName())) {
					out.writeObject(new BeaconMessage(
						BeaconMessage.LIST_RESPONSE, 
						user.getName(), 
						user.getAddress(), 
						user.getPort()));					

					Object[] args = new Object[1];

					synchronized (fileList) {
						Iterator iter = fileList.iterator();
						while (iter.hasNext()) {
							File tmpFile = (File) iter.next();
							args[0] = tmpFile.getName();
							out.writeObject(new BeaconMessage(
								BeaconMessage.LIST_FILE, 
								args, 
								user.getName(), 
								user.getAddress(), 
								user.getPort()));
						}		
						out.writeObject(new BeaconMessage(
							BeaconMessage.END_LIST, 
							user.getName(), 
							user.getAddress(), 
							user.getPort()));
					}
				}
				else {
					out.writeObject(new BeaconMessage(
						BeaconMessage.INVALID_TOKEN, 
						user.getName(), 
						user.getAddress(), 
						user.getPort()));
				}
			}
			catch (IOException ie) {
				System.out.println(ie);
				ie.printStackTrace();
			}
		}

		private void handleSearchGet(BeaconMessage msg, ObjectOutputStream out, ObjectInputStream in) {
			try {
				BeaconState user = new BeaconState(msg.getName(), msg.getAddress(), msg.getPort());
				Token tok = (Token) msg.getArg(0);
				String filename = (String) msg.getArg(1);
				Integer hopCount = (Integer) msg.getArg(2);

				if (hopCount.intValue() == 0) {	//hopCount goes to zero, so we are done. 
					out.writeObject(new BeaconMessage(
						BeaconMessage.SEARCH_FILE_NOT_FOUND, 
						msg.getName(), 
						msg.getAddress(), 
						msg.getPort()));
					return;
				}

				//decrement hopCount and then repack
				int oldHop = hopCount.intValue();
				oldHop--;
				Integer newHopCount = new Integer(oldHop);

				if (validate(tok, msg.getName())) {
					if (validateFilename(filename) ) {
						out.writeObject(new BeaconMessage(
							BeaconMessage.SEARCH_FILE_FOUND, 
							name, 
							address, 
							port));
					}
					else {
						//we don't have the file, so pass on to whoever we know
						BeaconState state = null;
						Iterator iter = users.iterator();
						while (iter.hasNext() ) {
							state = (BeaconState) iter.next();

							//Don't ask the person who asked us.
							if (state.equals(user)) {
								continue;
							}

							Socket userConn = new Socket(state.getAddress(), state.getPort());
							ObjectOutputStream userOut = new ObjectOutputStream (userConn.getOutputStream());
							out.flush();
							ObjectInputStream userIn = new ObjectInputStream(userConn.getInputStream());
						
							//send them original message with new hopcount
							Object[] args = new Object[3];
							args[0] = tok;
							args[1] = filename;	
							args[2] = newHopCount;
							userOut.writeObject(new BeaconMessage(
								BeaconMessage.SEARCHGET, 
								args, 
								msg.getName(), 
								msg.getAddress(), 
								msg.getPort()));
							
							//get response
							Object o = userIn.readObject();
							BeaconMessage m = (BeaconMessage) o;
							if (m.getType() == BeaconMessage.SEARCH_FILE_FOUND) {
								out.writeObject(new BeaconMessage(
									BeaconMessage.SEARCH_FILE_FOUND,
									m.getName(), 
									m.getAddress(), 
									m.getPort()));
								break;
							}
						}
						//out of loop and never found the file
						out.writeObject(new BeaconMessage(
							BeaconMessage.SEARCH_FILE_NOT_FOUND, 
							msg.getName(), 
							msg.getAddress(), 
							msg.getPort()));
						return;
					}
				}
				else {
					out.writeObject(new BeaconMessage(
						BeaconMessage.INVALID_TOKEN, 
						msg.getName(), 
						msg.getAddress(), 
						msg.getPort()));
				}
			}
			catch (OptionalDataException ode) {
				System.out.println(ode);
			}
			catch (ClassNotFoundException cnfe) {
				System.out.println(cnfe);
			}
			catch (SocketException se) {
				//If we fail to connect to a particular user,
            //we just return not found.
				try {
					out.writeObject(new BeaconMessage(
						BeaconMessage.SEARCH_FILE_NOT_FOUND, 
						msg.getName(), 
						msg.getAddress(), 
						msg.getPort()));
				}
				catch (IOException ie) { /* And if this fails we ignore */ }
			}
			catch (IOException ie) {
				System.out.println(ie);
				ie.printStackTrace();
				System.out.println("Failed to connect to the other client.");
			}
		}

		private void handlePut(BeaconMessage msg, ObjectOutputStream out, ObjectInputStream in) {
			/* Got PUT request, send OK and then transfer */
			try {
				Token token = (Token)(msg.getArg(0));		
				String filename = (String) msg.getArg(1);

				if (validate(token, msg.getName())) {
					out.writeObject(new BeaconMessage(
						BeaconMessage.OK, 
						msg.getName(), 
						msg.getAddress(), 
						msg.getPort()));

					FileOutputStream fileOut = new FileOutputStream(new File(filename));
					StreamCopier.copy(conn.getInputStream(), fileOut);
				}
				else {
					out.writeObject(new BeaconMessage(
						BeaconMessage.INVALID_TOKEN, 
						msg.getName(), 
						msg.getAddress(), 
						msg.getPort()));
				}
			}
			catch (IOException ioe) {
				System.out.println(ioe);
				ioe.printStackTrace();	
			}
		}

		private void handleGet(BeaconMessage msg, ObjectOutputStream out, ObjectInputStream in) {
			/* Got GET request, send OK and then start transfer */
			try {
				BeaconState user = new BeaconState(msg.getName(), msg.getAddress(), msg.getPort());
				Token token = (Token)(msg.getArg(0));		
				String filename = (String) msg.getArg(1);

				if (validate(token, msg.getName())) {
					if (validateFilename(filename)) {
						//everything checks out so we start sending file by first sending a put command:
						out.writeObject(new BeaconMessage(
							BeaconMessage.OK, 
							msg.getName(), 
							msg.getAddress(), 
							msg.getPort()));
					
						//start sending file
						FileInputStream fileIn = new FileInputStream(new File(filename));
						StreamCopier.copy(fileIn, conn.getOutputStream());
					}
					else {
						out.writeObject(new BeaconMessage(
							BeaconMessage.INVALID_FILENAME, 
							msg.getName(), 
							msg.getAddress(), 
							msg.getPort()));
					}
				}
				else {
					out.writeObject(new BeaconMessage(
						BeaconMessage.INVALID_TOKEN, 
						msg.getName(), 
						msg.getAddress(), 
						msg.getPort()));
				}
			}
			catch (OptionalDataException ode) {
				System.out.println(ode);
				ode.printStackTrace();
			}
			catch (IOException ioe) {
				System.out.println(ioe);
				ioe.printStackTrace();	
			}
		}
	} //end MessageHandler class
		
	/* 
		Now we are acting as a client.  We recieve commands via
	   our simple shell and transmit the corresponding messages.
		Depending on the command we may also need to send back 
		reponse messages.
	*/

	class BeaconShell extends Thread {
		private BufferedReader in;			//where we get commands from
		private final int MAX_ARGS = 4;	//max num args for a shell command

		//shell commands
		private final int OPEN      = 0;
		private final int LIST      = 1;
		private final int GET       = 2;
		private final int PUT       = 3;
		private final int CLOSE     = 4;
		private final int USERS     = 5;
		private final int EXIT      = 6;
		private final int HELP      = 7;
		private final int WHOAMI    = 8;
		private final int SEARCHGET = 9;
		private final int INVALID   = 10;
		private final int VALIDATE  = 11;
	
		BeaconShell () {
			in = new BufferedReader(new InputStreamReader(System.in), 1);
		}

		public void run() {
			String[] args;
			int type = -1;
			String command = new String();

			printPrompt();
			try {
				while (true) {
					args = new String[MAX_ARGS];	//reset args each time
					command = in.readLine();
					type = parse(command, args);
		
					switch (type) {
						case OPEN:
							open();
							break;
						case LIST:
							list(args[0]);
							break;
						case GET:
							get(args[0], args[1]);
							break;
						case PUT:
							put(args[0], args[1]);
							break;
						case CLOSE:
							close();
							break;
						case SEARCHGET:
							searchget(args[0], args[1]);
							break;
						case USERS:
							showUsers();
							break;
						case VALIDATE:
							validateCmd();
							break;
						case EXIT:
							logout();
							System.exit(0);
							break;
						case WHOAMI:
							whoami();
							break;
						case HELP:
							printHelp();
							break;
						default:
							System.out.println("Invalid command, type \"help\" for list of commands");
							break;
					}
					printPrompt();
				}
			}
			catch (IOException ie) {  
				System.out.println(ie);
				System.out.println("hit return to try to continue...");
			}
		}

		private BeaconState findBeaconState(String username) {
			//Gets BeaconState object referenced by username
			BeaconState state;
			synchronized (users) {
				Iterator iter = users.iterator();
				while (iter.hasNext()) {
					state = (BeaconState) iter.next();
					if (state.getName().equals(username) )
						return state;	
				}
				return null;
			}
		}
	
		private void whoami() {
			System.out.println(name);
		}

		private void open() {
			curToken = authenticate(name, key);
			if (curToken == null) 
				System.out.println("Open failed; check your username and key.");
			else
				System.out.println("Recieved token. Session started.");
		}

		private void list(String username) {
			if (username == null || username.equals("")) {
				System.out.println("You must provide a name.");
				return;
			}
		
			//get BeaconState and token for this user
			BeaconState user = findBeaconState(username);
			
			if (user == null) {
				System.out.println("Invalid user.");	
				return;
			}
			
			try {
				Socket conn = new Socket (user.getAddress(), user.getPort() );
				ObjectInputStream in = new ObjectInputStream ( conn.getInputStream() );
				ObjectOutputStream out = new ObjectOutputStream ( conn.getOutputStream() );
	
				//send list request
				Object[] args = new Object[1];
				args[0] = curToken;
				out.writeObject(new BeaconMessage(
					BeaconMessage.LIST, 
					args, 
					name, 
					address, 
					port ));
			
				//read in response
				Object o = in.readObject();
				BeaconMessage m = (BeaconMessage) o;
		
				if (m.getType() == BeaconMessage.INVALID_TOKEN) {
					System.out.println("Server responded: Invalid token");
					return;
				}
				else if (m.getType() == BeaconMessage.LIST_RESPONSE) {
					while ( true ) {
						o = in.readObject();
						m = (BeaconMessage) o;
						if (m.getType() == BeaconMessage.END_LIST)
							break; 
						System.out.println(m.getArg(0));	
					}
				}
				else if (m.getType() == BeaconMessage.END_LIST) {
					System.out.println("This beacon has no files to share.  Silly freeloader.");	
				}
				else {
					System.out.println("Invalid response from server.");
					return;
				}
			}
			catch (OptionalDataException ode) {
				System.out.println(ode);
				return;
			}
			catch (ClassNotFoundException cnfe) {
				System.out.println(cnfe);
				return;
			}
			catch (SocketException se) {
				System.out.println(se);
				System.out.println("Failed to connect to the other client.");
				return;
			}
			catch (IOException ie) {
				System.out.println(ie);
				System.out.println("Failed to connect to the other client.");
				return;
			}
		}

		private void put(String username, String filename) {
			if (username == null || username.equals ("") ) {
				System.out.println("You must provide a name.");
				return;
			}
			if (filename == null || filename.equals("")) {
				System.out.println("You must provide a filename.");
				return;
			}

			File tmpFile = new File (filename);
			if (!tmpFile.exists()) {
				System.out.println("Invalid file.");
				return;
			}

			BeaconState user = findBeaconState(username);

			if (user == null) {
				System.out.println("Invalid user.");	
				return;
			}

			try {
				Socket conn = new Socket(user.getAddress(), user.getPort() );
				ObjectInputStream in = new ObjectInputStream(conn.getInputStream());
				ObjectOutputStream out = new ObjectOutputStream(conn.getOutputStream());

				Object[] args = new Object[2];
				args[0] = curToken;
				args[1] = filename;
				out.writeObject(new BeaconMessage(
					BeaconMessage.PUT, 
					args, 
					name, 
					address, 
					port));

				//recieve response
				Object o = in.readObject();
				BeaconMessage m = (BeaconMessage) o;

				if (m.getType() == BeaconMessage.INVALID_TOKEN) {
					System.out.println("Server responded: Invalid token");
					return;
				}
				else if (m.getType() == BeaconMessage.INVALID_FILENAME) {
					System.out.println("Server responded: Invalid filename");
					return;
				}
				else if (m.getType() == BeaconMessage.OK) {
					//start sending file
					FileInputStream fileIn = new FileInputStream(new File (filename));
					StreamCopier.copy(fileIn, conn.getOutputStream());
					System.out.println("File sent.");
				}
				else {
					System.out.println("Invalid response recieved from server");
					return;
				}
			}
			catch (OptionalDataException ode) {
				System.out.println(ode);
				return;
			}
			catch (ClassNotFoundException cnfe) {
				System.out.println(cnfe);
				return;
			}
			catch (SocketException se) {
				System.out.println(se);
				System.out.println("Failed to connect to other client");
			}
			catch (FileNotFoundException fnfe) {
				System.out.println("We don't have " + filename);
				return;
			}
			catch (IOException ie) {
				System.out.println(ie);
				ie.printStackTrace();
				System.out.println("Failed to connect to other client");
			}
		}

		private void get(String username, String filename) {
			if (username == null || username.equals("") ) {
				System.out.println("You must provide a name.");
				return;
			}
			if (filename == null || filename.equals("")) {
				System.out.println("You must provide a filename.");
				return;
			}

			BeaconState user = findBeaconState(username);
			if (user == null) {
				System.out.println("Invalid user.");	
				return;
			}
		
			try {
				Socket conn = new Socket(user.getAddress(), user.getPort());
				ObjectInputStream in = new ObjectInputStream(conn.getInputStream());
				ObjectOutputStream out = new ObjectOutputStream(conn.getOutputStream());

				//send GET request
				Object[] args = new Object[2];
				args[0] = curToken;
				args[1] = filename;
				out.writeObject(new BeaconMessage(
					BeaconMessage.GET, 
					args, 
					name, 
					address, 
					port));

				//recieve response
				Object o = in.readObject();
				BeaconMessage m = (BeaconMessage) o;

				if (m.getType() == BeaconMessage.INVALID_TOKEN) {
					System.out.println("Server responded: Invalid token");
					return;
				}
				else if (m.getType() == BeaconMessage.INVALID_FILENAME) {
					System.out.println("Server responded: Invalid filename");
					return;
				}
				else if (m.getType() == BeaconMessage.OK) {

					FileOutputStream fileOut = new FileOutputStream(new File(filename));
					StreamCopier.copy(conn.getInputStream() , fileOut);
					System.out.println("Saved file as: " + filename);
				}
				else {
					System.out.println("Invalid response recieved from server");
					return;
				}
			}
			catch (OptionalDataException ode) {
				System.out.println(ode);
				return;
			}
			catch (ClassNotFoundException cnfe) {
				System.out.println(cnfe);
				return;
			}
			catch (SocketException se) {
				System.out.println(se);
				System.out.println("Failed to connect to other client");
			}
			catch (IOException ie) {
				System.out.println(ie);
				System.out.println("Failed to connect to other client");
			}
		}

		private void close() {
			revoke(curToken);
			System.out.println("Session ended.");
		}
	
		private void searchget(String filename, String hops) {
			if (filename == null || filename.equals("")) {
				System.out.println("You must provide a filename.");
				return;
			}
			if (hops == null) {
				System.out.println("You must provide a hopcount.");
				return;
			}

			Integer hopCount = null;
			try { 
				hopCount = new Integer(Integer.parseInt(hops)); 
			}
			catch (Exception e) { 
				System.out.println("Invalid hopcount; must be an integer"); 
				return; 
			}

			System.out.println("Searching ....");

			BeaconState state = null;
			BeaconMessage msg = null;
			boolean found = false;
			try {
				Iterator iter = users.iterator();
				while (iter.hasNext()) {
					state = (BeaconState) iter.next();
					Socket userConn = new Socket(state.getAddress(), state.getPort());
					ObjectOutputStream out = new ObjectOutputStream(userConn.getOutputStream());
					out.flush();
					ObjectInputStream in = new ObjectInputStream(userConn.getInputStream());

					Object[] args = new Object[3];
					args[0] = curToken;
					args[1] = filename;
					args[2] = hopCount;
								
					out.writeObject(new BeaconMessage(
						BeaconMessage.SEARCHGET, 
						args, 
						name, 
						address, 
						port));	

					Object o = in.readObject();
					msg = (BeaconMessage) o;
					if (msg.getType() == BeaconMessage.SEARCH_FILE_FOUND) {
						found = true;
						break;	
					}
					else if (msg.getType() == BeaconMessage.INVALID_TOKEN) {
						System.out.println("Invalid token.");
						return;
					}
				}
	
				if (found) {
					Socket userConn = new Socket(msg.getAddress(), msg.getPort());		
					ObjectOutputStream out = new ObjectOutputStream(userConn.getOutputStream());
					out.flush();
					ObjectInputStream in = new ObjectInputStream(userConn.getInputStream());

					Object[] args = new Object[2];
					args[0] = curToken;	
					args[1] = filename;
					out.writeObject(new BeaconMessage(
						BeaconMessage.GET, 
						args, 
						name, 
						address, 
						port));

					Object o = in.readObject();
					BeaconMessage response = (BeaconMessage) o;

					if (response.getType() == BeaconMessage.INVALID_TOKEN) {
						System.out.println("Server responded: Invalid token");
						return;
					}
					else if (response.getType() == BeaconMessage.INVALID_FILENAME) {
						System.out.println("Server responded: Invalid filename");
						return;
					}
					else if (response.getType() == BeaconMessage.OK) {

						//file is coming so setup Fis stream here.
						FileOutputStream fileOut = new FileOutputStream(new File(filename));
						StreamCopier.copy(userConn.getInputStream() , fileOut);
						System.out.println("Saved file as: " + filename);
					}
					else {
						System.out.println("Invalid response recieved from server");
						System.out.println(response);
						return;
					}
				}
				else {			//not found
					System.out.println("The file was not found. ");
				}
			}
			catch (OptionalDataException ode) {
				System.out.println(ode);
				return;
			}
			catch (ClassNotFoundException cnfe) {
				System.out.println(cnfe);
				return;
			}
			catch (SocketException se) {
				System.out.println(se);
				System.out.println("Failed to connect to the other client.");
				return;
			}
			catch (IOException ie) {
				System.out.println(ie);
				System.out.println("Failed to connect to the other client.");
			}
		}

		private void validateCmd() {
			boolean valid = validate(curToken, name);
			if (valid)
				System.out.println("Our token is still VALID");
			else
				System.out.println("Our token is INVALID.");
		}

		private void showUsers() {
			BeaconState state;
			synchronized (users) {
				Iterator iter = users.iterator();
				while (iter.hasNext()) {
					state = (BeaconState) iter.next();
					System.out.println(state);
				}
				System.out.println("");
			}
		}

		private final void printPrompt() {
			System.out.print("$ ");
		}

		private int parse (String command, String[] args) {
			//get type of command and args from raw 'command' string
			StringTokenizer tok = new StringTokenizer (command);

			String t;
			try {
				t = tok.nextToken();
				int i = 0;
				while (tok.hasMoreTokens()) {
					args[i] = tok.nextToken();
					i++;
				}
			}
			catch (NoSuchElementException nsee) {
				return INVALID;
			}
			
			if (t.equalsIgnoreCase("open"))
				return OPEN;
			else if (t.equalsIgnoreCase("list"))
				return LIST;
			else if (t.equalsIgnoreCase("get"))
				return GET;
			else if (t.equalsIgnoreCase("put"))
				return PUT;
			else if (t.equalsIgnoreCase("close"))
				return CLOSE;
			else if (t.equalsIgnoreCase("whoami"))
				return WHOAMI;
			else if (t.equalsIgnoreCase("searchget"))
				return SEARCHGET;
			else if (t.equalsIgnoreCase("validate"))
				return VALIDATE;
			else if (t.equalsIgnoreCase("users") || t.equals("w"))
				return USERS;
			else if (t.equalsIgnoreCase("help") || t.equals("?"))
				return HELP;
			else if (t.equalsIgnoreCase("exit") || t.equalsIgnoreCase("quit"))
				return EXIT;
			else
				return INVALID;
		}

		private void printHelp() {
			System.out.println("------------------------------------");
			System.out.println("BeaconShell v1.0 David Tate Jan 2001");
			System.out.println("------------------------------------");
			System.out.println("open ");
			System.out.println("    Gets an authorization token from key server.");
			System.out.println("list 'username'");
			System.out.println("    Lists all the files that our connection with ");
			System.out.println("    'username' can give us.");
			System.out.println("get 'username' 'filename'");
			System.out.println("    Recieve the 'filename' from user 'username' ");
			System.out.println("put 'username' 'filename'");
			System.out.println("    Send the 'filename' to user 'username' ");
			System.out.println("searchget 'filename' 'hopcount'");
			System.out.println("    Search the network for filename.");
			System.out.println("    Stop searching after hopcount hops between beacons.");
			System.out.println("close ");
			System.out.println("    Close the session.");
			System.out.println("validate");
			System.out.println("    See if our token is still valid.");
			System.out.println("users");
			System.out.println("    List all online users");
			System.out.println("exit");
			System.out.println("    Logout and exit.");
			System.out.println("whoami");
			System.out.println("    Print our username.");
			System.out.println("help");
			System.out.println("    Print this command menu.");
			System.out.println("------------------------------------");
			
		}
	} //end BeaconShell
	
	public static void main(String[] args) {
		String name = new String();
		String key = new String();
		String[] servAddress = null;
		String[] servPort = null;

		try {
			name = args[0];
			key = args[1];
			
			int numServers = Integer.parseInt(args[2]);
			servAddress = new String[numServers];
			servPort = new String[numServers];
			for (int i = 0, argIndex = 3; i < numServers; i++, argIndex += 2) {
				servAddress[i] = args[argIndex];
				servPort[i] = args[argIndex + 1];
			}
		}
		catch (Exception e) {
			System.out.println ("Usage: java BeaconClient name key numServers (serverAddress serverPort)*"); 
			System.exit(1);
		}
		BeaconClient b = new BeaconClient(name, key, servAddress, servPort);
	}
}

