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.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; /** * 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 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 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 GameData currentGame; private BlockingQueue actionQueue = new LinkedBlockingQueue(); 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() { while (true) { Runnable runner; try { runner = actionQueue.take(); if (runner == null) { return; } runner.run(); } catch (InterruptedException e) { } } } }).start(); run(new ConnectRunner()); } @Override public void disconnect() { connectedEvent = new Event(); connectionFailedEvent = new Event1(); run(new Runnable() { @Override public void run() { if (connection != null) { connection.disconnect(); connection = null; } ConnectionControl.this.run(null); } }); } @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 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 void offerGame(GameData data) { offeredGame = data; currentGame = data; sendGameOffer(); } @Override public void withdrawGame() { offeredGame = null; final UUID uuid = currentGame.getGameID(); currentGame = null; 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 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) { 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")); 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("round_state_update")) { roundStateUpdateEvent.emit((IRoundState) Base64.decodeToObject(extension .getValue("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")); 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.CANCEL && 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); return null; // 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; } } } } } private abstract class SendRunner implements Runnable { @Override public void run() { if (connection != null) { DefaultPacketExtension extension = createJRummikubExtension(); addData(extension); Message message = createMessage(extension); modifyMessage(message); connection.sendPacket(message); } } abstract protected void addData(DefaultPacketExtension extension); protected void modifyMessage(Message message) { } } }