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.ConnectionListener; 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 connectionFailedEvent = new Event1(); private Event1 gameOfferEvent = new Event1(); private Event1 gameWithdrawalEvent = new Event1(); private Event1 gameJoinEvent = new Event1(); private Event1 gameLeaveEvent = new Event1(); private Event1 gameJoinAckEvent = new Event1(); private Event2 changeColorEvent = new Event2(); private Event gameStartEvent = new Event(); private Event roundStartEvent = new Event(); private Event redealEvent = new Event(); private Event1 roundStateUpdateEvent = new Event1(); private Event1 tableUpdateEvent = new Event1(); private Event2 turnEndEvent = new Event2(); private Event nextPlayerEvent = new Event(); private Event turnStartEvent = new Event(); private Event1 participantLeftEvent = new Event1(); private Event connectionLostEvent = new Event(); private GameData currentGame; private BlockingQueue actionQueue = new LinkedBlockingQueue(); private volatile GameData offeredGame; private volatile String nickname; /** * Creates new connection control * * @param loginData * player's login data */ public ConnectionControl(LoginData loginData) { this.loginData = loginData; } @Override public String getNickname() { return nickname; } @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(); 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 getConnectionFailedEvent() { return connectionFailedEvent; } @Override public IEvent1 getGameOfferEvent() { return gameOfferEvent; } @Override public IEvent1 getGameWithdrawalEvent() { return gameWithdrawalEvent; } @Override public IEvent1 getGameJoinEvent() { return gameJoinEvent; } @Override public IEvent1 getGameLeaveEvent() { return gameLeaveEvent; } @Override public IEvent1 getGameJoinAckEvent() { return gameJoinAckEvent; } @Override public IEvent2 getChangeColorEvent() { return changeColorEvent; } @Override public IEvent getGameStartEvent() { return gameStartEvent; } @Override public IEvent getRoundStartEvent() { return roundStartEvent; } @Override public IEvent getRedealEvent() { return redealEvent; } @Override public IEvent1 getRoundStateUpdateEvent() { return roundStateUpdateEvent; } @Override public IEvent1 getTableUpdateEvent() { return tableUpdateEvent; } @Override public IEvent2 getTurnEndEvent() { return turnEndEvent; } @Override public IEvent getNextPlayerEvent() { return nextPlayerEvent; } @Override public IEvent getTurnStartEvent() { return turnStartEvent; } @Override public IEvent1 getParticipantLeftEvent() { return participantLeftEvent; } @Override public IEvent getConnectionLostEvent() { return connectionLostEvent; } @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 void emitLater(final Event1 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); config.setReconnectionAllowed(false); 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) { error = doFindNickname(); } if (error == null) { connection.addConnectionListener(new DisconnectListener()); 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); 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 LoginError doFindNickname() { final Message message = muc.createMessage(); PacketListener listener = new PacketListener() { @Override public void processPacket(Packet packet) { if (packet.getPacketID().equals(message.getPacketID())) { synchronized (ConnectRunner.this) { nickname = StringUtils.parseResource(packet.getFrom()); ConnectRunner.this.notify(); } } } }; synchronized (this) { connection.addPacketListener(listener, new PacketTypeFilter( Message.class)); connection.sendPacket(message); try { wait(5000); } catch (InterruptedException e) { } } connection.removePacketListener(listener); return null; } private class DisconnectListener implements ConnectionListener { @Override public void connectionClosed() { emitLater(connectionLostEvent); } @Override public void connectionClosedOnError(Exception arg0) { connectionClosed(); } @Override public void reconnectingIn(int arg0) { } @Override public void reconnectionFailed(Exception arg0) { } @Override public void reconnectionSuccessful() { } } 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 { Message message; public SendRunner() { DefaultPacketExtension extension = createJRummikubExtension(); addData(extension); message = createMessage(extension); modifyMessage(message); } @Override public void run() { // For thread safety Connection theConnection = connection; if (theConnection != null) { theConnection.sendPacket(message); } } abstract protected void addData(DefaultPacketExtension extension); protected void modifyMessage(Message message) { } } }