This repository has been archived on 2025-03-02. You can view files and clone it, but cannot push or open issues or pull requests.
JRummikub/src/jrummikub/control/network/ConnectionControl.java
Matthias Schiffer d2df76cae4 Withdraw games on start
git-svn-id: svn://sunsvr01.isp.uni-luebeck.de/swproj13/trunk@580 72836036-5685-4462-b002-a69064685172
2011-06-22 07:04:13 +02:00

804 lines
21 KiB
Java

package jrummikub.control.network;
import java.awt.Color;
import java.io.Serializable;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.swing.SwingUtilities;
import jrummikub.control.RoundControl.InvalidTurnInfo;
import jrummikub.model.GameSettings;
import jrummikub.model.IRoundState;
import jrummikub.model.ITable;
import jrummikub.model.PlayerSettings;
import jrummikub.util.Event;
import jrummikub.util.Event1;
import jrummikub.util.Event2;
import jrummikub.util.GameData;
import jrummikub.util.IEvent;
import jrummikub.util.IEvent1;
import jrummikub.util.IEvent2;
import jrummikub.util.LoginData;
import jrummikub.view.LoginError;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketExtensionFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.DefaultPacketExtension;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.packet.XMPPError.Type;
import org.jivesoftware.smack.util.Base64;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.muc.DiscussionHistory;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.ParticipantStatusListener;
/**
* Connection control managing network connections, messages and events
*/
public class ConnectionControl implements IConnectionControl {
private static class TurnEndData implements Serializable {
private static final long serialVersionUID = -3800572117130220737L;
private IRoundState roundState;
private InvalidTurnInfo invalidTurnInfo;
TurnEndData(IRoundState roundState, InvalidTurnInfo invalidTurnInfo) {
this.roundState = roundState;
this.invalidTurnInfo = invalidTurnInfo;
}
IRoundState getRoundState() {
return roundState;
}
InvalidTurnInfo getInvalidTurnInfo() {
return invalidTurnInfo;
}
}
private final static String ELEMENT_NAME = "rummikub";
private final static String NAMESPACE = "http://home.universe-factory.net/rummikub/";
private final static Runnable STOP_ACTION = new Runnable() {
@Override
public void run() {
}
};
private void fixGameSettings(GameSettings settings) {
for (PlayerSettings player : settings.getPlayerList()) {
if (player.getType() == PlayerSettings.Type.HUMAN) {
player.setType(PlayerSettings.Type.NETWORK);
}
if (player.getType() == PlayerSettings.Type.NETWORK
&& player.getName().equals(getNickname())) {
player.setType(PlayerSettings.Type.HUMAN);
}
}
}
private LoginData loginData;
private volatile Connection connection;
private volatile MultiUserChat muc;
private Event connectedEvent = new Event();
private Event1<LoginError> connectionFailedEvent = new Event1<LoginError>();
private Event1<GameData> gameOfferEvent = new Event1<GameData>();
private Event1<UUID> gameWithdrawalEvent = new Event1<UUID>();
private Event1<String> gameJoinEvent = new Event1<String>();
private Event1<String> gameLeaveEvent = new Event1<String>();
private Event1<Boolean> gameJoinAckEvent = new Event1<Boolean>();
private Event2<String, Color> changeColorEvent = new Event2<String, Color>();
private Event gameStartEvent = new Event();
private Event roundStartEvent = new Event();
private Event redealEvent = new Event();
private Event1<IRoundState> roundStateUpdateEvent = new Event1<IRoundState>();
private Event1<ITable> tableUpdateEvent = new Event1<ITable>();
private Event2<IRoundState, InvalidTurnInfo> turnEndEvent = new Event2<IRoundState, InvalidTurnInfo>();
private Event nextPlayerEvent = new Event();
private Event turnStartEvent = new Event();
private Event1<String> participantLeftEvent = new Event1<String>();
private GameData currentGame;
private BlockingQueue<Runnable> actionQueue = new LinkedBlockingQueue<Runnable>();
private volatile GameData offeredGame;
/**
* Creates new connection control
*
* @param loginData
* player's login data
*/
public ConnectionControl(LoginData loginData) {
this.loginData = loginData;
}
@Override
public String getNickname() {
return muc.getNickname();
}
@Override
public void connect() {
new Thread(new Runnable() {
@Override
public void run() {
Runnable runner = null;
do {
try {
runner = actionQueue.take();
runner.run();
} catch (InterruptedException e) {
}
} while (runner != STOP_ACTION);
}
}).start();
run(new ConnectRunner());
}
@Override
public void disconnect() {
final Connection theConnection = connection;
connection = null;
connectedEvent = new Event();
connectionFailedEvent = new Event1<LoginError>();
run(new Runnable() {
@Override
public void run() {
if (theConnection != null) {
theConnection.disconnect();
}
ConnectionControl.this.run(STOP_ACTION);
}
});
}
@Override
public IEvent getConnectedEvent() {
return connectedEvent;
}
@Override
public IEvent1<LoginError> getConnectionFailedEvent() {
return connectionFailedEvent;
}
@Override
public IEvent1<GameData> getGameOfferEvent() {
return gameOfferEvent;
}
@Override
public IEvent1<UUID> getGameWithdrawalEvent() {
return gameWithdrawalEvent;
}
@Override
public IEvent1<String> getGameJoinEvent() {
return gameJoinEvent;
}
@Override
public IEvent1<String> getGameLeaveEvent() {
return gameLeaveEvent;
}
@Override
public IEvent1<Boolean> getGameJoinAckEvent() {
return gameJoinAckEvent;
}
@Override
public IEvent2<String, Color> getChangeColorEvent() {
return changeColorEvent;
}
@Override
public IEvent getGameStartEvent() {
return gameStartEvent;
}
@Override
public IEvent getRoundStartEvent() {
return roundStartEvent;
}
@Override
public IEvent getRedealEvent() {
return redealEvent;
}
@Override
public IEvent1<IRoundState> getRoundStateUpdateEvent() {
return roundStateUpdateEvent;
}
@Override
public IEvent1<ITable> getTableUpdateEvent() {
return tableUpdateEvent;
}
@Override
public IEvent2<IRoundState, InvalidTurnInfo> getTurnEndEvent() {
return turnEndEvent;
}
@Override
public IEvent getNextPlayerEvent() {
return nextPlayerEvent;
}
@Override
public IEvent getTurnStartEvent() {
return turnStartEvent;
}
@Override
public IEvent1<String> getParticipantLeftEvent() {
return participantLeftEvent;
}
@Override
public void offerGame(GameData data) {
offeredGame = data;
currentGame = data;
sendGameOffer();
}
@Override
public void withdrawGame() {
offeredGame = null;
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "game_withdrawal");
extension.setValue("uuid", uuid.toString());
}
});
}
@Override
public GameData getCurrentGame() {
return currentGame;
}
@Override
public void setCurrentGame(GameData game) {
this.currentGame = game;
}
private void run(Runnable runner) {
while (true) {
try {
actionQueue.put(runner);
return;
} catch (InterruptedException e) {
}
}
}
@Override
public void joinGame(final GameData game) {
setCurrentGame(game);
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "game_join");
extension.setValue("uuid", game.getGameID().toString());
}
});
}
@Override
public void leaveGame() {
final UUID uuid = currentGame.getGameID();
currentGame = null;
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "game_leave");
extension.setValue("uuid", uuid.toString());
}
});
}
@Override
public void ackJoinGame(final String recipient, final boolean ack) {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "game_join_ack");
extension.setValue("uuid", uuid.toString());
extension.setValue("ack", Boolean.toString(ack));
}
@Override
protected void modifyMessage(Message message) {
message.setType(Message.Type.normal);
message.setTo(muc.getRoom() + "/" + recipient);
}
});
}
@Override
public void changeColor(final Color color) {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "change_color");
extension.setValue("uuid", uuid.toString());
extension.setValue("color", Base64.encodeObject(color, Base64.GZIP));
}
});
}
@Override
public void startGame() {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "game_start");
extension.setValue("uuid", uuid.toString());
}
});
}
@Override
public void startRound() {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "round_start");
extension.setValue("uuid", uuid.toString());
}
});
}
@Override
public void redeal() {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "redeal");
extension.setValue("uuid", uuid.toString());
}
});
}
@Override
public void updateRoundState(final IRoundState roundState) {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "round_state_update");
extension.setValue("uuid", uuid.toString());
extension.setValue("state",
Base64.encodeObject(roundState, Base64.GZIP));
}
});
}
@Override
public void updateTable(final ITable table) {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "table_update");
extension.setValue("uuid", uuid.toString());
extension.setValue("table", Base64.encodeObject(table, Base64.GZIP));
}
});
}
@Override
public void endTurn(final IRoundState state,
final InvalidTurnInfo invalidTurnInfo) {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "turn_end");
extension.setValue("uuid", uuid.toString());
extension.setValue("data", Base64.encodeObject(new TurnEndData(state,
invalidTurnInfo), Base64.GZIP));
}
});
}
@Override
public void nextPlayer() {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "next_player");
extension.setValue("uuid", uuid.toString());
}
});
}
@Override
public void startTurn() {
final UUID uuid = currentGame.getGameID();
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "turn_start");
extension.setValue("uuid", uuid.toString());
}
});
}
private void sendGameOffer() {
final GameData data = offeredGame;
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "game_offer");
extension.setValue("uuid", data.getGameID().toString());
extension.setValue("gameSettings",
Base64.encodeObject(data.getGameSettings(), Base64.GZIP));
}
});
}
private void requestGames() {
run(new SendRunner() {
@Override
protected void addData(DefaultPacketExtension extension) {
extension.setValue("messageType", "game_request");
}
});
}
private static void emitLater(final Event event) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
event.emit();
}
});
}
private static <T> void emitLater(final Event1<T> event, final T arg) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
event.emit(arg);
}
});
}
private Message createMessage(PacketExtension extension) {
Message message = muc.createMessage();
message.addExtension(extension);
return message;
}
private static DefaultPacketExtension createJRummikubExtension() {
return new DefaultPacketExtension(ELEMENT_NAME, NAMESPACE);
}
private void processPacket(Packet packet) {
if (connection == null) {
return;
}
DefaultPacketExtension extension = (DefaultPacketExtension) packet
.getExtension(ELEMENT_NAME, NAMESPACE);
if (((Message) packet).getType() == Message.Type.error) {
System.err.println("Received error message from '" + packet.getFrom()
+ "'");
return;
}
String sender = packet.getFrom();
sender = sender.substring(sender.indexOf('/') + 1);
String messageType = extension.getValue("messageType");
handleMessageTypes(extension, sender, messageType);
}
private void handleMessageTypes(DefaultPacketExtension extension,
String sender, String messageType) {
if (messageType.equals("game_offer")) {
UUID uuid = UUID.fromString(extension.getValue("uuid"));
GameSettings settings = (GameSettings) Base64.decodeToObject(extension
.getValue("gameSettings"));
fixGameSettings(settings);
GameData gameData = new GameData(uuid, settings, sender);
gameOfferEvent.emit(gameData);
} else if (messageType.equals("game_withdrawal")) {
gameWithdrawalEvent.emit(UUID.fromString(extension.getValue("uuid")));
} else if (messageType.equals("game_request")) {
if (offeredGame != null) {
sendGameOffer();
}
} else if (currentGame != null) {
UUID uuid = UUID.fromString(extension.getValue("uuid"));
if (!currentGame.getGameID().equals(uuid)) {
return;
}
messagesDuringGame(extension, sender, messageType);
}
}
private void messagesDuringGame(DefaultPacketExtension extension,
String sender, String messageType) {
if (messageType.equals("game_join")) {
gameJoinEvent.emit(sender);
} else if (messageType.equals("game_leave")) {
gameLeaveEvent.emit(sender);
} else if (messageType.equals("game_join_ack")) {
gameJoinAckEvent.emit(Boolean.valueOf(extension.getValue("ack")));
} else if (messageType.equals("change_color")) {
changeColorEvent.emit(sender,
(Color) Base64.decodeToObject(extension.getValue("color")));
} else if (messageType.equals("game_start")) {
gameStartEvent.emit();
} else if (messageType.equals("round_start")) {
roundStartEvent.emit();
} else if (messageType.equals("redeal")) {
redealEvent.emit();
} else {
messagesDuringRound(extension, messageType);
}
}
private void messagesDuringRound(DefaultPacketExtension extension,
String messageType) {
if (messageType.equals("round_state_update")) {
IRoundState state = (IRoundState) Base64.decodeToObject(extension
.getValue("state"));
fixGameSettings(state.getGameSettings());
roundStateUpdateEvent.emit(state);
} else if (messageType.equals("table_update")) {
tableUpdateEvent.emit((ITable) Base64.decodeToObject(extension
.getValue("table")));
} else if (messageType.equals("turn_end")) {
TurnEndData data = (TurnEndData) Base64.decodeToObject(extension
.getValue("data"));
fixGameSettings(data.getRoundState().getGameSettings());
turnEndEvent.emit(data.getRoundState(), data.getInvalidTurnInfo());
} else if (messageType.equals("next_player")) {
nextPlayerEvent.emit();
} else if (messageType.equals("turn_start")) {
turnStartEvent.emit();
} else {
System.err.println("Received unrecognized message of type '"
+ messageType + "'");
}
}
private class ConnectRunner implements Runnable {
@Override
public void run() {
ConnectionConfiguration config = new ConnectionConfiguration(
loginData.getServerName());
config.setSendPresence(false);
config.setRosterLoadedAtLogin(false);
config.setCompressionEnabled(true);
connection = new XMPPConnection(config);
connection.addPacketListener(new PacketListener() {
@Override
public void processPacket(final Packet packet) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
ConnectionControl.this.processPacket(packet);
}
});
}
}, new AndFilter(new PacketTypeFilter(Message.class),
new PacketExtensionFilter(ELEMENT_NAME, NAMESPACE)));
LoginError error = doConnect();
if (error == null) {
error = doLogin();
}
if (error == null) {
error = doJoin();
}
if (error == null) {
requestGames();
emitLater(connectedEvent);
} else {
connection.disconnect();
connection = null;
emitLater(connectionFailedEvent, error);
}
}
private LoginError doConnect() {
try {
connection.connect();
return null;
} catch (XMPPException e) {
XMPPError xmppError = e.getXMPPError();
if (xmppError != null) {
if (xmppError.getType() == Type.WAIT && xmppError.getCode() == 504) {
return LoginError.UNKNOWN_HOST;
}
}
e.printStackTrace();
return LoginError.UNKNOWN_ERROR;
}
}
private LoginError doLogin() {
try {
connection.login(loginData.getUserName(), loginData.getPassword(),
"JRummikub-" + StringUtils.randomString(8));
return null;
} catch (XMPPException e) {
return LoginError.AUTHENTICATION_FAILED;
}
}
private LoginError doJoin() {
muc = new MultiUserChat(connection, loginData.getChannelName());
DiscussionHistory history = new DiscussionHistory();
history.setMaxStanzas(0);
String nickname = loginData.getUserName();
// Loop until a unused nickname is found
while (true) {
try {
muc.join(nickname, null, history, 10000);
break; // Join was successful, break the loop
} catch (XMPPException e) {
XMPPError xmppError = e.getXMPPError();
if (xmppError != null && xmppError.getType() == Type.CANCEL
&& xmppError.getCode() == 409) {
// There was a conflict, try again with another
// nickname
nickname += "_";
continue;
} else {
// An unknown error has occurred, cancel connect
if (xmppError != null && xmppError.getType() == Type.CANCEL
&& xmppError.getCode() == 404) {
return LoginError.UNKNOWN_CHANNEL;
}
e.printStackTrace();
return LoginError.UNKNOWN_ERROR;
}
}
}
muc.addParticipantStatusListener(new LeaveListener());
return null;
}
private class LeaveListener implements ParticipantStatusListener {
@Override
public void voiceRevoked(String arg0) {
}
@Override
public void voiceGranted(String arg0) {
}
@Override
public void ownershipRevoked(String arg0) {
}
@Override
public void ownershipGranted(String arg0) {
}
@Override
public void nicknameChanged(String arg0, String arg1) {
}
@Override
public void moderatorRevoked(String arg0) {
}
@Override
public void moderatorGranted(String arg0) {
}
@Override
public void membershipRevoked(String arg0) {
}
@Override
public void membershipGranted(String arg0) {
}
@Override
public void left(String participant) {
participant = participant.substring(participant.indexOf('/') + 1);
emitLater(participantLeftEvent, participant);
}
@Override
public void kicked(String participant, String actor, String reason) {
left(participant);
}
@Override
public void joined(String arg0) {
}
@Override
public void banned(String participant, String arg1, String arg2) {
left(participant);
}
@Override
public void adminRevoked(String arg0) {
}
@Override
public void adminGranted(String arg0) {
}
}
}
private abstract class SendRunner implements Runnable {
@Override
public void run() {
// For thread safety
Connection theConnection = connection;
if (theConnection != null) {
DefaultPacketExtension extension = createJRummikubExtension();
addData(extension);
Message message = createMessage(extension);
modifyMessage(message);
theConnection.sendPacket(message);
}
}
abstract protected void addData(DefaultPacketExtension extension);
protected void modifyMessage(Message message) {
}
}
}