package net.sf.colossus.webclient;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.colossus.util.Glob;
import net.sf.colossus.webcommon.GameInfo;
import net.sf.colossus.webcommon.IWebClient;
import net.sf.colossus.webcommon.IWebServer;
import net.sf.colossus.webcommon.User;
/**
* This implements the webserver/client communication at client side.
* It implements the server interface on client side;
* i.e. something server wanted to execute for a client, is read
* from the client socket input stream, parsed, and executed
* by the (WebClient)SocketThread.
*
* This also contains the methods which are called by the client
* (WebClient's GUI) and are sent over the socket to the server
* (note that those calls mostly happen in the EDT).
*
* @author Clemens Katzer
*/
public class WebClientSocketThread extends Thread implements IWebServer
{
private static final Logger LOGGER = Logger
.getLogger(WebClientSocketThread.class.getName());
private IWebClient webClient = null;
private final HashMap<String, GameInfo> gameHash;
private String hostname = null;
private final int port;
private String username = null;
private String password = null;
private boolean force = false;
private String email = null;
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private boolean stillNeedsRun = true;
private final static String sep = IWebServer.WebProtocolSeparator;
private boolean loggedIn = false;
private AckWaiter ackWaiter;
private WcstException failedException = null;
private static int counter = 0;
private static WebClientSocketThread currentAttempt = null;
private boolean closingForcefullyToCancel = false;
private final static Object connectOngoingMutex = new Object();
// TODO also defined in webserver.WebServerConstants!
private final Charset charset = Charset.forName("UTF-8");
public WebClientSocketThread(IWebClient wcGUI, String hostname, int port,
String username, String password, boolean force, String email,
String confCode, HashMap<String, GameInfo> gameHash)
{
super("WebClientSocketThread for user " + username + "-" + counter);
counter++;
this.webClient = wcGUI;
this.gameHash = gameHash;
this.hostname = hostname;
LOGGER.info("WCST constructor: user " + username + " host " + hostname
+ " port " + port + " password " + password);
this.port = port;
this.username = username;
this.password = password;
this.force = force;
this.email = email;
this.ackWaiter = new AckWaiter();
net.sf.colossus.util.InstanceTracker
.register(this, "WCST " + username);
try
{
synchronized (connectOngoingMutex)
{
WebClientSocketThread.currentAttempt = this;
}
// connect, as well as any of the three below
// might throw an exception if they fail:
connect();
synchronized (connectOngoingMutex)
{
WebClientSocketThread.currentAttempt = null;
}
// If client GUI provided a confirmation code, then in is a
// confirmation for a previously sent registration; otherwise:
// If client GUI provided an email, it's a registration attempt,
// otherwise just a normal login.
if (confCode != null)
{
// confirmation
confirm(confCode);
}
else if (email != null)
{
// initial registration
register();
}
else
{
// no email, login instead
login();
}
}
catch (WcstException e)
{
this.failedException = e;
}
}
public String getOneLine() throws IOException
{
String line = "No line - got exception!";
try
{
line = this.in.readLine();
}
catch (IOException e)
{
LOGGER.log(Level.SEVERE, "Exception during read from socket!", e);
Thread.dumpStack();
throw e;
}
return line;
}
public WcstException getException()
{
return failedException;
}
public static void cancelConnectAttempt()
{
synchronized (connectOngoingMutex)
{
if (currentAttempt != null)
{
currentAttempt.closeSocketForcefully();
currentAttempt = null;
}
else
{
LOGGER.warning("no point to cancel, currentAttempt is null.");
}
}
}
private void closeSocketForcefully()
{
if (socket != null)
{
try
{
closingForcefullyToCancel = true;
socket.close();
}
catch (IOException e)
{
// ignore any exception
}
}
}
private void connect() throws WcstException
{
String info = null;
writeLog("About to connect client socket to " + hostname + ":" + port);
try
{
InetSocketAddress address = new InetSocketAddress(hostname, port);
socket = new Socket();
// 10.000 = 10 seconds timeout
socket.connect(address, 10000);
if (socket != null)
{
out = new PrintWriter(
new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream(), charset)), true);
}
else
{
LOGGER.warning("socket null - creating printWriter skipped!");
}
}
catch (UnknownHostException e)
{
info = "Unknown host: " + e.getMessage()
+ "\" - wrong address/server name?";
writeLog(e.toString());
}
catch (SocketTimeoutException e)
{
info = "Could not connect: '" + e.getMessage()
+ "' - possibly a firewall blocking port " + port + "?";
}
catch (ConnectException e)
{
info = "Could not connect: '" + e.getMessage()
+ "' - wrong address/port, or server not running?";
writeLog(e.toString());
}
catch (Exception e) // IOException, IllegalBlockingModeException
{
if (closingForcefullyToCancel)
{
info = "Connect attempt interrupted/cancelled "
+ "(closingForcefullyToCancel flag is set): "
+ e.toString();
writeLog(e.toString());
throw new WcstException(info, false, true);
}
info = "Exception during connect: " + e.toString();
writeLog(e.toString());
}
if (info != null)
{
String message = info;
throw new WcstException(message);
}
}
/**
* Initial registration attempt
*
* @throws WcstException
*/
private void register() throws WcstException
{
String info = null;
try
{
this.in = new BufferedReader(new InputStreamReader(
socket.getInputStream(), charset));
send(RegisterUser + sep + username + sep + password + sep + email);
String fromServer = null;
if ((fromServer = getOneLine()) != null)
{
if (fromServer.startsWith("ACK:"))
{
// ("WCST.register(): ok, got ACK! ("+fromServer+")");
}
else
{
// TODO: why do we handle this with NACKs ?
String prefix = "NACK: " + IWebServer.RegisterUser + sep;
if (fromServer.startsWith(prefix))
{
info = fromServer.substring(prefix.length());
}
else
{
info = fromServer;
}
}
}
else
{
info = "NULL reply from server (socket closed??)!";
}
}
catch (Exception ex)
{
writeLog(ex.toString());
info = "Creating or reading from buffered reader failed";
}
if (info != null)
{
LOGGER.info("register() : info != null, info: " + info);
String message = info;
throw new WcstException(message);
}
}
/**
* Send the confirmation code
* @throws WcstException
*/
private void confirm(String confCode) throws WcstException
{
String info = null;
try
{
this.in = new BufferedReader(new InputStreamReader(
socket.getInputStream(), charset));
send(ConfirmRegistration + sep + username + sep + confCode);
String fromServer = null;
if ((fromServer = getOneLine()) != null)
{
if (fromServer.startsWith("ACK:"))
{
// ("WCST.confirm(): ok, got ACK! ("+fromServer+")");
}
else
{
String prefix = "NACK: " + IWebServer.ConfirmRegistration
+ sep;
if (fromServer.startsWith(prefix))
{
info = fromServer.substring(prefix.length());
}
else
{
info = fromServer;
}
}
}
else
{
info = "NULL reply from server (socket closed??)!";
}
}
catch (Exception ex)
{
writeLog(ex.toString());
info = "Creating or reading from buffered reader failed";
}
if (info != null)
{
throw new WcstException(info);
}
}
private void login() throws WcstException
{
String info = null;
boolean duplicateLogin = false;
boolean cancelled = false;
try
{
this.in = new BufferedReader(new InputStreamReader(
socket.getInputStream(), charset));
int version = webClient.getClientVersion();
send(Login + sep + username + sep + password + sep + force + sep
+ version);
String fromServer = null;
if ((fromServer = getOneLine()) != null)
{
if (fromServer.startsWith("ACK:"))
{
loggedIn = true;
}
else if (fromServer.equals("NACK: " + IWebServer.Login + sep
+ IWebClient.alreadyLoggedIn))
{
duplicateLogin = true;
info = "Already logged in!";
}
else
{
String prefix = "NACK: " + IWebServer.Login + sep;
if (fromServer.startsWith(prefix))
{
info = fromServer.substring(prefix.length());
}
else
{
info = fromServer;
}
}
}
else
{
info = "NULL reply from server (socket closed??)!";
}
}
catch (Exception ex)
{
writeLog(ex.toString());
info = "Creating or reading from buffered reader failed";
}
if (info != null)
{
String message = "Login failed: " + info;
throw new WcstException(message, duplicateLogin, cancelled);
}
}
// Needed even if instantiation failed, otherwise the GC
// won't clean up this thread if it was not run
// TODO perhaps that is not needed any more in Java 1.5 ?
public boolean stillNeedsRun()
{
return stillNeedsRun;
}
public String getUsername()
{
return username;
}
public String getUserEmail()
{
return email;
}
@Override
public void run()
{
String threadName = Thread.currentThread().getName();
stillNeedsRun = false;
if (this.socket == null)
{
// All right. We were just called to get the run()
// done, even if the constructor threw exception,
// for example could not connect.
// Socket would be null if server closes us quickly
// enough. It seems it can happen that that is not the,
// case, so make sure we bail out even if server does not
// kick us out quickly enough...
LOGGER.info(threadName + ": socket null, cleanup+return");
doCleanup();
return;
}
if (this.failedException != null)
{
// All right. We were just called to get the run()
// done, even if the constructor threw exception.
// It seems it can happen that socket == null is not always
// the case, so make sure we bail out even if server does not
// kick us out quickly enough...
LOGGER.info(threadName + ": failedException set, cleanup+return");
doCleanup();
return;
}
LOGGER.info(threadName + ": everything normal, going to run loop!");
String fromServer = null;
boolean done = false;
boolean forcedLogout = false;
try
{
while (!done && (fromServer = getOneLine()) != null)
{
String[] tokens = fromServer.split(sep, -1);
String command = tokens[0];
if (fromServer.startsWith("ACK: "))
{
command = tokens[0].substring(5);
handleAckNack(command, tokens);
}
else if (fromServer.startsWith("NACK: "))
{
command = tokens[0].substring(6);
handleAckNack(command, tokens);
}
else if (fromServer.equals(IWebClient.connectionClosed))
{
done = true;
}
else if (fromServer.equals(IWebClient.forcedLogout))
{
forcedLogout = true;
done = true;
}
else if (command.equals(IWebClient.gameInfo))
{
GameInfo gi = restoreGameInfo(tokens);
webClient.gameInfo(gi);
}
else if (command.equals(IWebClient.userInfo))
{
int loggedin = Integer.parseInt(tokens[1]);
int enrolled = Integer.parseInt(tokens[2]);
int playing = Integer.parseInt(tokens[3]);
int dead = Integer.parseInt(tokens[4]);
long ago = Long.parseLong(tokens[5]);
String text = tokens[6];
webClient.userInfo(loggedin, enrolled, playing, dead, ago,
text);
}
else if (command.equals(IWebClient.didEnroll))
{
String gameId = tokens[1];
String user = tokens[2];
webClient.didEnroll(gameId, user);
}
else if (command.equals(IWebClient.didUnenroll))
{
String gameId = tokens[1];
String user = tokens[2];
webClient.didUnenroll(gameId, user);
}
else if (command.equals(IWebClient.gameCancelled))
{
String gameId = tokens[1];
String byUser = tokens[2];
webClient.gameCancelled(gameId, byUser);
}
else if (command.equals(IWebClient.gameStartsSoon))
{
String gameId = tokens[1];
String startUser = tokens[2];
confirmCommand(command, gameId, startUser, "nothing");
webClient.gameStartsSoon(gameId, startUser);
}
else if (command.equals(IWebClient.gameStartsNow))
{
String gameId = tokens[1];
int port = Integer.parseInt(tokens[2]);
String host = tokens[3];
int checkIV = -1;
int warnIV = -1;
int timeout = -1;
if (tokens.length > 4)
{
checkIV = Integer.parseInt(tokens[4]);
warnIV = Integer.parseInt(tokens[5]);
timeout = Integer.parseInt(tokens[6]);
}
confirmCommand(command, gameId, port + "", host);
webClient.gameStartsNow(gameId, port, host, checkIV,
warnIV, timeout);
}
else if (command.equals(IWebClient.chatDeliver))
{
String chatId = tokens[1];
long when = Long.parseLong(tokens[2]);
String sender = tokens[3];
String message = tokens[4];
boolean resent = Boolean.valueOf(tokens[5]).booleanValue();
webClient.chatDeliver(chatId, when, sender, message,
resent);
}
else if (command.equals(IWebClient.pingRequest))
{
String arg1 = tokens[1];
String arg2 = tokens[2];
String arg3 = tokens[3];
pingResponse(arg1, arg2, arg3);
}
else if (command.equals(IWebClient.generalMessage))
{
long when = Long.parseLong(tokens[1]);
boolean error = Boolean.valueOf(tokens[2]).booleanValue();
String title = tokens[3];
String message = tokens[4];
webClient.deliverGeneralMessage(when, error, title,
message);
}
else if (command.equals(IWebClient.requestAttention))
{
long when = Long.parseLong(tokens[1]);
String byUser = tokens[2];
boolean byAdmin = Boolean.valueOf(tokens[3])
.booleanValue();
String message = tokens[4];
int beepCount = Integer.parseInt(tokens[5]);
long beepInterval = Long.parseLong(tokens[6]);
boolean windows = Boolean.valueOf(tokens[7])
.booleanValue();
webClient.requestAttention(when, byUser, byAdmin, message,
beepCount, beepInterval, windows);
}
else if (command.equals(IWebClient.watchGameInfo))
{
String gameId = tokens[1];
String host = tokens[2];
int port = Integer.parseInt(tokens[3]);
webClient.watchGameInfo(gameId, host, port);
}
else if (command.equals(IWebClient.grantAdmin))
{
webClient.grantAdminStatus();
}
else if (command.equals(IWebClient.tellOwnInfo))
{
String email = tokens[1];
webClient.tellOwnInfo(email);
}
else
{
if (webClient != null)
{
if (webClient instanceof WebClient)
{
((WebClient)webClient).showAnswer(fromServer);
}
/*
else if (webClient instanceof WebClient)
{
((WebClient)webClient).showAnswer(fromServer);
}
*/
}
}
} // while !done && readLine != null
writeLog("End of SocketClientThread while loop, done = " + done
+ " readline "
+ (fromServer == null ? " null " : "'" + fromServer + "'"));
if (loggedIn)
{
// Unexpectedly got a connection closed, at least we did not
// initiate the logout ourself. So, to be sure, reset the GUI
// to be "empty".
webClient.connectionReset(forcedLogout);
}
}
catch (IOException ex)
{
LOGGER.log(Level.SEVERE, "WebClientSocketThread IOException!");
webClient.connectionReset(false);
}
catch (Exception e)
{
LOGGER.log(Level.WARNING,
"WebClientSocketThread whatever Exception!", e);
Thread.dumpStack();
webClient.connectionReset(false);
}
doCleanup();
}
private GameInfo restoreGameInfo(String[] tokens)
{
GameInfo gi = GameInfo.fromString(tokens, gameHash, false);
return gi;
}
private void doCleanup()
{
if (socket != null)
{
try
{
socket.close();
}
catch (IOException ex)
{
LOGGER.log(Level.WARNING,
"WebClientSocketThread close() IOException!", ex);
}
}
socket = null;
webClient = null;
ackWaiter = null;
}
public void dispose()
{
doCleanup();
}
private void send(String s)
{
out.println(s);
}
private class AckWaiter
{
String command;
String result;
boolean waiting = false;
public AckWaiter()
{
// nothing special to do
}
public boolean isWaiting()
{
return waiting;
}
public synchronized String sendAndWait(String command, String args)
{
waiting = true;
setCommand(command);
send(command + sep + args);
// will wait() until SocketThread has set the result and called notify.
String result = waitForAck();
waiting = false;
return result;
}
public void setCommand(String command)
{
this.command = command;
}
public String getCommand()
{
return command;
}
public synchronized String waitForAck()
{
try
{
wait();
}
catch (InterruptedException e)
{
LOGGER.log(Level.WARNING, " got exception " + e.toString());
}
return result;
}
public synchronized void setResult(String result)
{
this.result = result;
this.notify();
}
}
public void logout()
{
loggedIn = false;
send(Logout);
}
public void messageToAdmin(long when, String senderName,
String senderMail, List<String> lines)
{
String listOfLines = Glob.glob(Glob.sep, lines);
send(MessageToAdmin + sep + when + sep + senderName + sep + senderMail
+ sep + listOfLines);
}
public String changeProperties(String username, String oldPW,
String newPW, String email, Boolean isAdminObj)
{
String reason = ackWaiter.sendAndWait(ChangePassword, username + sep
+ oldPW + sep + newPW + sep + email + sep + isAdminObj);
return reason;
}
private void handleAckNack(String command, String[] tokens)
{
if (ackWaiter != null && ackWaiter.isWaiting())
{
String cmd = ackWaiter.getCommand();
if (cmd != null && cmd.equals(command))
{
ackWaiter.setResult(tokens[1]);
}
else
{
LOGGER.log(Level.WARNING, "Waiting for (N)ACK for command "
+ cmd + " but " + "got " + command);
}
}
}
public GameInfo proposeGame(String initiator, String variant,
String viewmode, long startAt, int duration, String summary,
String expire, List<String> gameOptions, List<String> teleportOptions,
int min, int target, int max)
{
String gameOptionsString = Glob.glob(gameOptions);
String teleportOptionsString = Glob.glob(teleportOptions);
send(Propose + sep + initiator + sep + variant + sep + viewmode + sep
+ startAt + sep + duration + sep + summary + sep + expire + sep
+ gameOptionsString + sep + teleportOptionsString + sep + min
+ sep + target + sep + max);
return null;
}
public void enrollUserToGame(String gameId, String username)
{
send(Enroll + sep + gameId + sep + username);
}
public void unenrollUserFromGame(String gameId, String username)
{
send(Unenroll + sep + gameId + sep + username);
}
public void cancelGame(String gameId, String byUser)
{
send(Cancel + sep + gameId + sep + byUser);
}
public void startGame(String gameId, User byUser)
{
send(Start + sep + gameId + sep + byUser.getName());
}
public void resumeGame(String gameId, String loadGame, User byUser)
{
send(Resume + sep + gameId + sep + loadGame + sep + byUser.getName());
}
public void deleteSuspendedGame(String gameId, User user)
{
send(DeleteSuspendedGame + sep + gameId + sep + user.getName());
}
public void informStartedByPlayer(String gameId)
{
send(StartedByPlayer + sep + gameId);
}
public void informLocallyGameOver(String gameId)
{
send(LocallyGameOver + sep + gameId);
}
public void startGameOnPlayerHost(String gameId, String hostingPlayer,
String playerHost, int port)
{
send(StartAtPlayer + sep + gameId + sep + hostingPlayer + sep
+ playerHost + sep + port);
}
public void chatSubmit(String chatId, String sender, String message)
{
String sending = ChatSubmit + sep + chatId + sep + sender + sep
+ message;
send(sending);
}
public void pingResponse(String arg1, String arg2, String arg3)
{
send(PingResponse + sep + arg1 + sep + arg2 + sep + arg3);
}
public void watchGame(String gameId, String username)
{
send(WatchGame + sep + gameId + sep + username);
}
public void sleepFor(long millis)
{
try
{
Thread.sleep(millis);
}
catch (InterruptedException e)
{
// ignore
}
}
public void confirmCommand(String cmd, String arg1, String arg2,
String arg3)
{
sleepFor(200);
long now = new Date().getTime();
send(ConfirmCommand + sep + now + sep + cmd + sep + arg1 + sep + arg2
+ sep + arg3);
}
public void requestUserAttention(long when, String sender,
boolean isAdmin, String recipient, String message, int beepCount,
long beepInterval, boolean windows)
{
String sending = RequestUserAttention + sep + when + sep + sender
+ sep + isAdmin + sep + recipient + sep + message + sep
+ beepCount + sep + beepInterval + sep + windows;
send(sending);
}
public void shutdownServer()
{
send(IWebServer.ShutdownServer);
}
public void rereadLoginMessage()
{
send(IWebServer.RereadLoginMessage);
}
public void dumpInfo()
{
send(IWebServer.DumpInfo);
}
public void submitAnyText(String text)
{
if (text.equals("die"))
{
System.exit(1);
}
send(text);
}
private void writeLog(String s)
{
if (true)
{
LOGGER.log(Level.INFO, s);
}
}
public class WcstException extends Exception
{
boolean failedBecauseAlreadyLoggedIn = false;
boolean wasCancelled = false;
public WcstException(String message, boolean dupl, boolean cancelled)
{
super(message);
failedBecauseAlreadyLoggedIn = dupl;
wasCancelled = cancelled;
}
public WcstException(String message)
{
super(message);
failedBecauseAlreadyLoggedIn = false;
}
public boolean failedBecauseAlreadyLoggedIn()
{
return failedBecauseAlreadyLoggedIn;
}
public boolean wasCancelled()
{
return wasCancelled;
}
}
}