package pictionary.pictionaryserver;
import pictionary.message.*;
import pictionary.message.Message.MessageType;
import pictionary.PictionaryPlayer;
import pictionary.PictionaryGame;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Iterator;
import java.util.Scanner;
import java.util.HashMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.ArrayList;
import javax.swing.*;
import java.util.HashSet;
import java.util.List;
/**
* Responsible for sending messages between the connected clients,
* and between the Pictionary game and the clients.
* Keeps track of the clients with a HashMap<String, ClientConnection> where String is the clients unique name;
* there is one {@link ClientConnection} per connected client.
*
* @author Kristoffer Nordkvist
*/
public final class PictionaryServer extends JFrame
{
/**
* The collection that stores all the client connections. Clients are identified with their unique name.
*/
private HashMap<String, ClientConnection> clientConnections = new HashMap<String, ClientConnection>();
/**
* The {@link PictionaryGame} instance used by the server.
*/
private PictionaryGame pictionaryGame;
/**
* The server's queue with incoming messages. Every time a {@link ClientConnection} receives a message,
* it puts that message in this queue.
*/
private LinkedBlockingQueue<ClientConnectionMessage> incomingMessages =
new LinkedBlockingQueue<ClientConnectionMessage>();
/**
* Responsible for accepting new connections.
*/
private ServerSocket serverSocket;
/**
* The {@link ServerThread} instance used for accepting and creating new client connections.
*/
private ServerThread serverThread;
/**
* True if the server thread is shut down, else false.
* Used in {@link ServerThread}.
*/
volatile private boolean shutdown;
/**
* The port on which this server is listening.
*/
private final int port;
/**
* This server's hostname.
*/
private final String host;
/**
* The button used when starting or shutting down the server.
*/
private JButton disconnectOrConnectBtn = new JButton("Connect");
/**
* True if the server is running, else false. Used by the server's GUI.
*/
private boolean running = false;
/**
* True if this server has been shut down, else false. Used by the server's GUI.
*/
private boolean wasShutDown = false;
/**
* A collection of usernames that are unavailable.
*/
private HashSet<String> illegalUsernames = new HashSet<String>();
/**
* The current word category in the Pictionary game.
*/
public final String category;
/**
* The words used in the Pictionary game.
*/
public final ArrayList<String> words;
/**
* The server's constructor.
* @param host The server's hostname.
* @param port The port that the server listens for new connections on.
* @param category The Pictionary game's word category.
* @param words The list of words to be used in the Pictionary game.
*/
public PictionaryServer(String host, int port, String category, ArrayList<String> words)
{
this.host = host;
this.port = port;
this.category = category;
this.words = words;
pictionaryGame = new PictionaryGame(category, words, 60);
disconnectOrConnectBtn.addActionListener(new ConnectButtonListener());
initIllegalNames();
this.addWindowListener(new CloseWindowListener());
this.add(disconnectOrConnectBtn);
this.setResizable(false);
this.setTitle("Server not started");
this.setSize(240, 100);
}
/**
* Clears the list of illegal names and adds some standard ones.
*/
private void initIllegalNames()
{
illegalUsernames.clear();
illegalUsernames.add("server");
illegalUsernames.add("admin");
illegalUsernames.add("");
}
/**
* Checks whether and username is valid or not.
* @param username The name to check for validity.
* @return True if the username is legal, else false.
*/
public boolean validusername(String username)
{
return username != null && !illegalUsernames.contains(username.toLowerCase());
}
/**
* Overrides the default behavior when the window is closed.
* @author Kristoffer Nordkvist
*/
private class CloseWindowListener extends WindowAdapter
{
/**
* Shuts down the server and exits the program.
*/
@Override
public void windowClosing(WindowEvent e)
{
shutDownServer();
System.exit(0);
}
}
/**
* The listener for {@link PictionaryServer#disconnectOrConnectBtn}.
* @author Kristoffer Nordkvist
*/
private class ConnectButtonListener implements ActionListener
{
/**
* Starts or stops the server.
*/
@Override
public void actionPerformed(ActionEvent ae)
{
if(!running)
{
try
{
disconnectOrConnectBtn.setText("Disconnect");
running = true;
PictionaryServer.this.setTitle("Up and running");
if(!wasShutDown)
{
connect();
}
else
{
restartServer(port);
}
}
catch (IOException e)
{
JOptionPane.showMessageDialog(PictionaryServer.this, "Could not start server, exiting.");
PictionaryServer.this.processWindowEvent(
new WindowEvent(
PictionaryServer.this, WindowEvent.WINDOW_CLOSING));
}
}
else
{
shutDownServer();
running = false;
wasShutDown = true;
disconnectOrConnectBtn.setText("Connect");
PictionaryServer.this.setTitle("Server stopped");
}
}
}
/**
* Creates a new ServerSocket and starts listening for new connections.
* @throws IOException If the ServerSocket cannot be created.
*/
private void connect() throws IOException
{
serverSocket = new ServerSocket(port, 0, InetAddress.getByName(host));
System.out.println("Listening for client connections on " + serverSocket.toString());
serverThread = new ServerThread();
serverThread.start();
Thread readerThread = new Thread()
{
public void run()
{
while (true)
{
try
{
ClientConnectionMessage msg = incomingMessages.take(); // Ta n�sta meddelande fr�n k�n.
messageReceived(msg.clientConnection, msg.message); // G�r n�got med meddelandet
}
catch (Exception e)
{
System.out.println("Exception while handling received message:");
e.printStackTrace();
}
}
}
};
readerThread.setDaemon(true);
readerThread.start();
}
/**
* Called when a new message has been received
* @param client The client that sent the message.
* @param message The message that was sent from the client.
*/
private void messageReceived(ClientConnection client, Message message)
{
if(message.messageType == MessageType.JoinGameRequest)
{
GameStatusData gameStatus = new GameStatusData(pictionaryGame.getPlayerNames(), pictionaryGame.getStatus());
sendToOne(client.clientID, Message.CreateDataMessage(MessageType.JoinGameMessage, gameStatus));
pictionaryGame.join(client);
}
else if(message.messageType == MessageType.Guess)
{
pictionaryGame.guessWord(client, (String)message.messageData.data);
}
// Klienten vill koppla ner.
else if(message.messageType == MessageType.DisconnectRequest)
{
System.out.println(client.getClientID() + " sent a disconnect request");
// Om spelaren var mitt i ett spel
if(pictionaryGame.playerExists(client))
{
System.out.println(client.getClientID() + " is being removed from the game");
// Informera spelet om att en klient har l�mnat
pictionaryGame.playerLeft(client);
}
clientDisconnected(client);
client.close();
}
else
{
sendToAll(message);
}
}
/**
* Gets the connected client's unique names.
* @return A list containing the unique identifiers for each client.
*/
synchronized private List<String> getClients()
{
List<String> clients = new ArrayList<String>();
Iterator<String> it = PictionaryServer.this.clientConnections.keySet().iterator();
while(it.hasNext())
{
clients.add(it.next());
}
return clients;
}
/**
* Clears the queue of incoming messages, shuts down the listener thread,
* sets the listener thread and ServerSocket to null.
*/
private void shutdownServerSocket()
{
if (serverThread == null)
return;
incomingMessages.clear();
shutdown = true;
try
{
serverSocket.close();
}
catch (IOException e){}
serverThread = null;
serverSocket = null;
}
/**
* Starts the server again by creating a new ServerSocket and listener thread.
* Should be called after {@link #shutDownServer()}.
* @param port The port which the server will listen on for new connections.
* @throws IOException If a new ServerSocket could not be created.
*/
private void restartServer(int port) throws IOException
{
initIllegalNames();
pictionaryGame = new PictionaryGame(category, words, 60);
if (serverThread != null && serverThread.isAlive())
throw new IllegalStateException("Server is already listening for connections.");
shutdown = false;
serverSocket = new ServerSocket(port);
serverThread = new ServerThread();
serverThread.start();
System.out.println("Server restarted. Listening for client connections on port " + port);
}
/**
* Disconnects all clients and stops accepting new connections.
*/
private void shutDownServer()
{
shutdownServerSocket();
sendToAll(new Message(MessageType.ServerDisconnecting));
try
{
Thread.sleep(1000);
}
catch (InterruptedException e) {}
for (ClientConnection clientConnection : clientConnections.values())
{
clientConnection.close();
}
}
/**
* Sends a message to all clients.
* @param message The message to be sent.
*/
synchronized private void sendToAll(Message message)
{
for (ClientConnection clientConnection : clientConnections.values())
{
clientConnection.send(message);
}
}
/**
* Sends a message to one client.
* @param recipient The receiving client.
* @param message The message to be sent.
* @return True if the message was sent, else false.
*/
synchronized private boolean sendToOne(String recipient, Message message)
{
ClientConnection clientConnection = clientConnections.get(recipient);
if (clientConnection == null)
{
return false;
}
else
{
clientConnection.send(message);
return true;
}
}
/**
* Adds a connection to {@link #clientConnections}, adds the client's name to {@link #illegalUsernames}
* and informs all connected clients that someone connected to the server.
* @param newConnection The client that connected to the server.
*/
synchronized private void acceptConnection(ClientConnection newConnection)
{
clientConnections.put(newConnection.clientID, newConnection);
illegalUsernames.add(newConnection.clientID.toLowerCase());
sendToAll(new Message(MessageType.AClientConnectedToServer, newConnection.clientID));
}
/**
* Removes a client from {@link #clientConnections}, removes the username for {@link #illegalUsernames}
* and informs all connected clients that someone left the server.
* @param client The client that has disconnected from the server.
*/
synchronized private void clientDisconnected(ClientConnection client)
{
if (clientConnections.containsKey(client.clientID))
{
System.out.println("clientDisconnected: " + client.clientID);
clientConnections.remove(client.clientID);
illegalUsernames.remove(client.clientID.toLowerCase());
Message message = new Message(MessageType.AClientDisconnected, client.clientID);
sendToAll(message);
}
}
/**
* Calls {@link #clientDisconnected(ClientConnection)}
* and prints a message to standard error output.
* @param client The client that disconnected.
* @param errorMessage The message to print.
*/
synchronized private void connectionToClientClosedWithError(ClientConnection client, String errorMessage)
{
clientDisconnected(client);
System.err.println(errorMessage);
}
/**
* A simple class containing a {@link ClientConnection} and a String message.
* @author Kristoffer Nordkvist
*/
private class ClientConnectionMessage
{
ClientConnection clientConnection;
Message message;
}
/**
* Listen for new clients,
* if a new client is found it is passed to {@link ClientConnection#ClientConnection(BlockingQueue, Socket)}
* along with {@link PictionaryServer#incomingMessages}.
* @author Kristoffer Nordkvist
*/
private class ServerThread extends Thread
{
/**
* Listens for and accepts new clients.
*/
@Override
public void run()
{
try
{
while (!shutdown)
{
Socket connection = serverSocket.accept();
new ClientConnection(incomingMessages, connection);
}
System.out.println("Listener socket has shut down gracefully.");
}
catch (Exception e)
{
if (shutdown)
System.out.println("Listener socket encountered and error while being shut down.");
else
System.out.println("Listener socket has been shut down by error: " + e);
}
}
}
/**
* Handles communication with one client; there is one ClientConnection per connected client.
* Also extends {@link PictionaryPlayer} so it can be used with {@link PictionaryServer#pictionaryGame}.
* Has one thread for receiving messages from the client and one for sending messages.
*/
private class ClientConnection extends PictionaryPlayer
{
/**
* The clients unique identifier, a String containing it's name.
*/
private String clientID;
/**
* Received messages are put in this collection,
* this is a reference to {@link PictionaryServer#incomingMessages}.
*/
private BlockingQueue<ClientConnectionMessage> incomingMessages;
/**
* A collection of messages waiting to be sent to the client.
*/
private LinkedBlockingQueue<Message> outgoingMessages;
/**
* The Socket used for getting the in and output streams.
*/
private Socket connection;
/**
* The stream used for reading messages from the client.
*/
private ObjectInputStream in;
/**
* The stream used for sending messages to the client.
*/
private ObjectOutputStream out;
/**
* Is set to true if the client should be disconnected gracefully.
*/
private volatile boolean closed;
/**
* The {@link SendThread} instance.
*/
private Thread sendThread;
/**
* Takes care of receiving and forwarding messages from the client to the server.
*/
private volatile Thread receiveThread;
/**
* The constructor.
* @param receivedMessageQueue The queue where received messages will be put.
* @param connection The Socket that will be used to get streams from.
*/
public ClientConnection(BlockingQueue<ClientConnectionMessage> receivedMessageQueue, Socket connection)
{
super(0, -1);
System.out.println("NEW ClientConnection");
this.connection = connection;
incomingMessages = receivedMessageQueue;
outgoingMessages = new LinkedBlockingQueue<Message>();
sendThread = new SendThread();
sendThread.start();
}
/**
* Get method for {@link #clientID}.
*/
public String getClientID()
{
return clientID;
}
/**
* Closes this connection and terminates the send and receive threads.
*/
private void close()
{
closed = true;
sendThread.interrupt();
if (receiveThread != null)
{
receiveThread.interrupt();
}
try
{
connection.close();
}
catch (IOException e) { }
}
/**
* Adds a message to {@link #outgoingMessages}.
* @param message The message to be sent.
*/
public void send(Message message)
{
outgoingMessages.add(message);
}
/**
* Sends the message to {@link PictionaryServer#connectionToClientClosedWithError(ClientConnection, String)}
* and calls {@link #close()}.
* @param message The message to be printed.
*/
private void closedWithError(String message)
{
connectionToClientClosedWithError(this, message);
close();
}
/**
* Initially handles setup,
* then takes care of sending messages to the client.
* @author Kristoffer Nordkvist
*/
private class SendThread extends Thread
{
/**
* This is where SendThread does all the work.
*/
@Override
public void run()
{
try
{
out = new ObjectOutputStream(connection.getOutputStream());
in = new ObjectInputStream(connection.getInputStream());
// Be klienten om ett anv�ndarnamn tills den ger oss ett som blir godk�nt.
while(true)
{
out.writeObject(new Message(MessageType.ServerRequestUsername));
out.flush();
Message message = (Message)in.readObject();
if(message == null)
{
ClientConnection.this.closedWithError("The client is not behaving");
return;
}
if(message.messageType == MessageType.ClientUsernameResponse)
{
// Anv�ndarnamnet �r OK.
if(validusername(message.client))
{
// Sluta be om ett anv�ndarnamn.
ClientConnection.this.clientID = message.client;
break;
}
}
}
acceptConnection(ClientConnection.this);
StatusData clients = new StatusData(getClients());
Message serverAccept = Message.CreateDataMessage(MessageType.ServerAcceptedConnection, clients);
out.writeObject(serverAccept);
out.flush();
receiveThread = new ReceiveThread();
receiveThread.start();
}
catch(Exception e)
{
try
{
closed = true;
connection.close();
}
catch (Exception e1)
{
}
System.out.println("Error while setting up connection:\n");
e.printStackTrace();
return;
}
try
{
// Get messages from outgoingMessages queue and send them.
while (!closed)
{
try
{
Message message = outgoingMessages.take();
out.reset();
out.writeObject(message);
out.flush();
/*if (message.messageType == MessageType.DisconnectRequest) // A signal to close the connection.
{
outgoingMessages.clear();
close();
}*/
}
catch (InterruptedException e)
{
// should mean that connection is closing
}
}
}
catch (IOException e)
{
if (!closed)
{
closedWithError("Error while sending data to client.");
System.out.println("PictionaryServer send thread terminated by IOException: " + e);
}
}
catch (Exception e)
{
if (!closed)
{
closedWithError("Internal Error: Unexpected exception in output thread: " + e);
System.out.println("\nUnexpected error shuts down PictionaryServer's send thread:");
e.printStackTrace();
}
}
}
} // end SendThread
/**
* Receives messages from the client and adds them to the server's message queue.
*/
private class ReceiveThread extends Thread
{
/**
* This is where ReceiveThread does all the work.
*/
@Override
public void run()
{
try
{
while (!closed)
{
try
{
Message message = (Message)in.readObject();
ClientConnectionMessage clientConnectionMessage = new ClientConnectionMessage();
clientConnectionMessage.clientConnection = ClientConnection.this;
clientConnectionMessage.message = message;
incomingMessages.put(clientConnectionMessage);
}
catch (InterruptedException e)
{
// should mean that connection is closing
return;
}
}
}
catch (IOException e)
{
if (!closed)
{
closedWithError("Error while reading data from client.");
System.out.println("PictionaryServer receive thread terminated by IOException: " + e);
}
}
catch (Exception e)
{
if (!closed)
{
closedWithError("Internal Error: Unexpected exception in input thread: " + e);
System.out.println("\nUnexpected error shuts down PictionaryServer's receive thread:");
e.printStackTrace();
}
}
}
}
//------------------------ PictionaryPlayer abstract method implementations
//--------------------------------------------------------------------------
@Override
public void stopDraw()
{
send(new Message(MessageType.StopDrawing));
}
@Override
public void playerStoppedDrawing(String clientID)
{
send(new Message(MessageType.AClientStoppedDrawing, clientID));
}
@Override
public void startDraw(String wordToDraw)
{
StartDrawData startDrawData = new StartDrawData(wordToDraw, pictionaryGame.drawTimeLimit);
Message theMessage =
Message.CreateDataMessage(MessageType.StartDrawing, startDrawData);
send(theMessage);
}
@Override
public void playerStartedDrawing(String clientID)
{
send(new Message(MessageType.AClientStartedDrawing, clientID));
}
@Override
public void gameStarted()
{
send(Message.CreateDataMessage(MessageType.ServerMessage, "A new game of pictionary started. " +
"The category is \"" + pictionaryGame.getCategory() + "\"."));
}
@Override
public void correctGuess(String guess)
{
send(Message.CreateDataMessage(MessageType.CorrectGuess, guess));
}
@Override
public void correctGuessBroadcast(String clientID, String guess)
{
send(Message.CreateDataMessage(MessageType.AClientCorrectGuess, clientID, guess));
}
@Override
public void startGuessing()
{
send(new Message(MessageType.StartGuessing));
}
@Override
public void stopGuessing()
{
send(new Message(MessageType.StopGuessing));
}
@Override
public void wrongGuess(String clientID, String guess)
{
send(Message.CreateDataMessage(MessageType.AClientIncorrectGuess, clientID, guess));
}
@Override
public void reportScores(List<String> players, List<Integer> scores)
{
StringBuilder message = new StringBuilder();
int highScore = -1;
for(int i = 0; i < players.size(); ++ i)
{
message.append(players.get(i) + " finished with " + scores.get(i) + " points.\n");
if(scores.get(i) > highScore)
{
highScore = scores.get(i);
}
}
ArrayList<String> winners = new ArrayList<String>();
for(int i = 0; i < scores.size(); ++i)
{
if(scores.get(i) == highScore)
{
winners.add(players.get(i));
}
}
if(winners.size() == 1)
{
message.append("The winner is " + winners.get(0) + " with " + highScore + " points.");
}
else
{
message.append("The winners, with " + highScore + " points, are:");
String delim = "\n";
for(String winner : winners)
{
message.append(delim).append(winner);
delim = ",\n";
}
}
send(Message.CreateDataMessage(MessageType.ServerMessage, message.toString()));
}
@Override
public void tellWord(String word)
{
send(Message.CreateDataMessage(MessageType.ServerMessage, "The word was \"" + word + "\""));
}
//--------------------------- end PictionaryPlayer abstract method implementations
//---------------------------------------------------------------------------------
} // end nested class ClientConnection
/**
* Tries to read settings.txt and then start the server.
* If settings.txt does not exist, a new one is created and we read that one instead.
*/
public static void main(String[] args)
{
final String currentDirectory = System.getProperty("user.dir");
File settingsFile = new File(currentDirectory + "\\settings.txt");
// Filen finns inte, skapa en ny med standarnv�rden
if(!settingsFile.exists())
{
final String newline = System.getProperty("line.separator");
try
{
FileWriter fstream = new FileWriter(settingsFile);
BufferedWriter writer = new BufferedWriter(fstream);
writer.write("comment#This is a generated version of the settings file.#" + newline);
writer.write("host#127.0.0.1#" + newline);
writer.write("port#2012#" + newline);
writer.write("category#Mixed categories#" + newline);
writer.write("wordfile#"+currentDirectory + "\\mixed.txt#");
writer.close();
}
catch (Exception ex)
{
System.err.println("Error: " + ex.getMessage());
}
System.err.println("The settings file was missing, but I have created a new one for you. You're welcome.");
} // end if
Scanner scanner;
String host = "";
int port = -1;
String category = "";
String wordFilePath = "";
boolean useDefault = false;
// L�s in inst�llningarna fr�n filen
try
{
scanner = new Scanner(new FileReader(settingsFile));
scanner.useDelimiter("#");
}
catch (FileNotFoundException fnfex)
{
System.err.println("The settings file coult not be found...");
useDefault = true;
return;
}
try
{
System.out.println("Reading settings from file:");
while ( scanner.hasNextLine() )
{
if ( scanner.hasNext() )
{
String name = scanner.next().trim();
String value = scanner.next().trim();
if(name.equals("comment"))
{
System.out.println("Comment: " + value.trim());
continue;
}
System.out.println("Setting name is '" + name + "' and value is '" + value + "'.");
if(name.equals("host")) host = value;
else if(name.equals("port")) port = Integer.parseInt(value);
else if(name.equals("category")) category = value;
else if(name.equals("wordfile")) wordFilePath = value;
}
else
{
break;
}
}
System.out.println("Finished reading settings file.");
}
catch (Exception ex)
{
useDefault = true;
}
if(useDefault)
{
useDefault = true;
System.err.println("The settings file coult not be read... Using default values instead.");
host = "127.0.0.1";
port = 2012;
category = "Mixed categories";
wordFilePath = currentDirectory + "\\mixed.txt";
}
ArrayList<String> words = new ArrayList<String>();
System.out.println("Trying to read words from the file '" + wordFilePath + "'.");
try
{
File wordFile = new File(wordFilePath);
scanner = new Scanner(new FileReader(wordFile));
while ( scanner.hasNextLine() )
{
words.add(scanner.nextLine());
}
System.out.println("Finished reading " + words.size() + " words.");
}
catch(Exception e)
{
System.err.println("Error reading word file, exiting.");
System.exit(ERROR);
}
finally
{
scanner.close();
final PictionaryServer server = new PictionaryServer(host, port, category, words);
EventQueue.invokeLater(new Runnable()
{
public void run()
{
server.setVisible(true);
}
});
}
}
}