/***************************************************************************
* Copyright 2006-2016 by Christian Ihle *
* contact@kouchat.net *
* *
* This file is part of KouChat. *
* *
* KouChat is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 3 of *
* the License, or (at your option) any later version. *
* *
* KouChat is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with KouChat. *
* If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
package net.usikkert.kouchat.net;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import net.usikkert.kouchat.misc.Controller;
import net.usikkert.kouchat.misc.User;
import net.usikkert.kouchat.misc.WaitingList;
import net.usikkert.kouchat.util.Sleeper;
import net.usikkert.kouchat.util.Validate;
/**
* Wrapper around a real {@link MessageResponder} that handles operations that need to be async and
* operations from unknown users.
*
* <p>As a rule, all operations are handled by a single thread, to keep the order they arrive.
* Some operations need to wait for a response, and must therefore be handled by a new thread to
* avoid locking other operations.</p>
*
* <p>Some operations handles users appearing unexpectedly, from a timeout, or because of packet loss.
* Those will add the user to a waiting list, ask the user to identify, and then wait for it to happen,
* before continuing.</p>
*
* @author Christian Ihle
*/
public class AsyncMessageResponderWrapper implements MessageResponder {
private final Sleeper sleeper = new Sleeper();
private final ExecutorService executorService = Executors.newCachedThreadPool();
private final MessageResponder messageResponder;
private final Controller controller;
private final WaitingList waitingList;
public AsyncMessageResponderWrapper(final MessageResponder messageResponder, final Controller controller) {
Validate.notNull(messageResponder, "MessageResponder can not be null");
Validate.notNull(controller, "Controller can not be null");
this.messageResponder = messageResponder;
this.controller = controller;
this.waitingList = controller.getWaitingList();
}
/**
* Shows a message. If the user that sent the message does not yet exist in the user list,
* the user is asked to identify itself before the message is shown.
*/
@Override
public void messageArrived(final int userCode, final String msg, final int color) {
// A little hack to stop messages from showing before the user is logged on
if (controller.isNewUser(userCode)) {
askUserToIdentify(userCode);
executorService.execute(new Runnable() {
@Override
public void run() {
waitForUserToIdentify(userCode);
messageResponder.messageArrived(userCode, msg, color);
}
});
}
else {
messageResponder.messageArrived(userCode, msg, color);
}
}
/**
* Topic changed. Asked to identify instead, if unknown.
*/
@Override
public void topicChanged(final int userCode, final String newTopic, final String nick, final long time) {
if (controller.isNewUser(userCode)) {
askUserToIdentify(userCode);
}
else {
messageResponder.topicChanged(userCode, newTopic, nick, time);
}
}
@Override
public void topicRequested() {
messageResponder.topicRequested();
}
/**
* User changed away state. Asked to identify instead, if unknown.
*/
@Override
public void awayChanged(final int userCode, final boolean away, final String awayMsg) {
if (controller.isNewUser(userCode)) {
askUserToIdentify(userCode);
}
else {
messageResponder.awayChanged(userCode, away, awayMsg);
}
}
/**
* User changed nick name. Asked to identify instead, if unknown.
*/
@Override
public void nickChanged(final int userCode, final String newNick) {
if (controller.isNewUser(userCode)) {
askUserToIdentify(userCode);
}
else {
messageResponder.nickChanged(userCode, newNick);
}
}
@Override
public void nickCrash() {
messageResponder.nickCrash();
}
@Override
public void meLogOn(final String ipAddress) {
messageResponder.meLogOn(ipAddress);
}
@Override
public void userLogOn(final User newUser) {
messageResponder.userLogOn(newUser);
}
@Override
public void userLogOff(final int userCode) {
messageResponder.userLogOff(userCode);
}
@Override
public void userExposing(final User user) {
messageResponder.userExposing(user);
}
@Override
public void exposeRequested() {
messageResponder.exposeRequested();
}
@Override
public void writingChanged(final int userCode, final boolean writing) {
messageResponder.writingChanged(userCode, writing);
}
@Override
public void meIdle(final String ipAddress) {
messageResponder.meIdle(ipAddress);
}
/**
* User reports to be idle. Asked to identify instead, if unknown.
*/
@Override
public void userIdle(final int userCode, final String ipAddress) {
if (controller.isNewUser(userCode)) {
askUserToIdentify(userCode);
}
else {
messageResponder.userIdle(userCode, ipAddress);
}
}
/**
* Receives a file transfer, which may take a long time. Needs to run in a different thread.
* Handles unidentified users.
*/
@Override
public void fileSend(final int userCode, final long byteSize, final String fileName,
final String user, final int fileHash) {
if (controller.isNewUser(userCode)) {
askUserToIdentify(userCode);
}
executorService.execute(new Runnable() {
@Override
public void run() {
waitForUserToIdentify(userCode);
messageResponder.fileSend(userCode, byteSize, fileName, user, fileHash);
}
});
}
@Override
public void fileSendAborted(final int userCode, final String fileName, final int fileHash) {
messageResponder.fileSendAborted(userCode, fileName, fileHash);
}
/**
* Does the actual file transfer to the other user, which may take a long time. Needs to run
* in a different thread.
*/
@Override
public void fileSendAccepted(final int userCode, final String fileName, final int fileHash, final int port) {
executorService.execute(new Runnable() {
@Override
public void run() {
messageResponder.fileSendAccepted(userCode, fileName, fileHash, port);
}
});
}
@Override
public void clientInfo(final int userCode, final String client, final long timeSinceLogon,
final String operatingSystem, final int privateChatPort) {
messageResponder.clientInfo(userCode, client, timeSinceLogon, operatingSystem, privateChatPort);
}
/**
* Asks user with the specified userCode to identify with {@link #userExposing(User)}.
* Adds user to waiting list so we know this user sent a message without being known,
* and also so we can wait for this user to identify before continuing an operation.
*/
void askUserToIdentify(final int userCode) {
waitingList.addWaitingUser(userCode);
controller.sendExposeMessage();
controller.sendGetTopicMessage();
}
/**
* Waits for user with the specified userCode to identify in {@link #userExposing(User)}.
* Gives up after 2 seconds.
*/
void waitForUserToIdentify(final int userCode) {
int counter = 0;
while (waitingList.isWaitingUser(userCode) && counter < 40) {
counter++;
sleeper.sleep(50);
}
}
}