/* * * Copyright (c) 2000-2013 by Rodney Kinney, Brent Easton * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.chat.node; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; import java.util.Properties; import org.apache.commons.codec.binary.Base64; import VASSAL.Info; import VASSAL.build.GameModule; import VASSAL.chat.CgiServerStatus; import VASSAL.chat.Compressor; import VASSAL.chat.InviteCommand; import VASSAL.chat.InviteEncoder; import VASSAL.chat.LockableChatServerConnection; import VASSAL.chat.LockableRoom; import VASSAL.chat.MainRoomChecker; import VASSAL.chat.Player; import VASSAL.chat.PlayerEncoder; import VASSAL.chat.PrivateChatEncoder; import VASSAL.chat.PrivateChatManager; import VASSAL.chat.Room; import VASSAL.chat.ServerStatus; import VASSAL.chat.SimplePlayer; import VASSAL.chat.SimpleRoom; import VASSAL.chat.SimpleStatus; import VASSAL.chat.SoundEncoder; import VASSAL.chat.SynchEncoder; import VASSAL.chat.WelcomeMessageServer; import VASSAL.chat.messageboard.Message; import VASSAL.chat.messageboard.MessageBoard; import VASSAL.chat.ui.ChatControlsInitializer; import VASSAL.chat.ui.ChatServerControls; import VASSAL.chat.ui.InviteAction; import VASSAL.chat.ui.KickAction; import VASSAL.chat.ui.LockableRoomTreeRenderer; import VASSAL.chat.ui.MessageBoardControlsInitializer; import VASSAL.chat.ui.PrivateMessageAction; import VASSAL.chat.ui.RoomInteractionControlsInitializer; import VASSAL.chat.ui.SendSoundAction; import VASSAL.chat.ui.ServerStatusControlsInitializer; import VASSAL.chat.ui.ShowProfileAction; import VASSAL.chat.ui.SimpleStatusControlsInitializer; import VASSAL.chat.ui.SynchAction; import VASSAL.command.Command; import VASSAL.command.CommandEncoder; import VASSAL.i18n.Resources; import VASSAL.tools.ArrayUtils; import VASSAL.tools.PropertiesEncoder; import VASSAL.tools.SequenceEncoder; /** * @author rkinney */ public abstract class NodeClient implements LockableChatServerConnection, PlayerEncoder, ChatControlsInitializer { public static final String ZIP_HEADER = "!ZIP!"; //$NON-NLS-1$ protected PropertyChangeSupport propSupport = new PropertyChangeSupport(this); protected NodePlayer me; protected SimpleRoom currentRoom; protected String defaultRoomName = DEFAULT_ROOM_NAME; //$NON-NLS-1$ protected NodeRoom[] allRooms = new NodeRoom[0]; protected MessageBoard msgSvr; protected WelcomeMessageServer welcomer; protected ServerStatus serverStatus; protected String moduleName; protected String playerId; protected MainRoomChecker checker = new MainRoomChecker(); protected int compressionLimit = 1000; protected CommandEncoder encoder; protected MessageBoardControlsInitializer messageBoardControls; protected RoomInteractionControlsInitializer roomControls; protected ServerStatusControlsInitializer serverStatusControls; protected SimpleStatusControlsInitializer playerStatusControls; protected SoundEncoder soundEncoder; protected PrivateChatEncoder privateChatEncoder; protected SynchEncoder synchEncoder; protected InviteEncoder inviteEncoder; protected PropertyChangeListener nameChangeListener; protected PropertyChangeListener profileChangeListener; protected NodeRoom pendingSynchToRoom; public NodeClient(String moduleName, String playerId, CommandEncoder encoder, MessageBoard msgSvr, WelcomeMessageServer welcomer) { this.encoder = encoder; this.msgSvr = msgSvr; this.welcomer = welcomer; this.playerId = playerId; this.moduleName = moduleName; serverStatus = new CgiServerStatus(); me = new NodePlayer(playerId); messageBoardControls = new MessageBoardControlsInitializer(Resources .getString("Chat.messages"), msgSvr); //$NON-NLS-1$ roomControls = new LockableNodeRoomControls(this); roomControls.addPlayerActionFactory(ShowProfileAction.factory()); roomControls.addPlayerActionFactory(SynchAction.factory(this)); PrivateChatManager privateChatManager = new PrivateChatManager(this); roomControls.addPlayerActionFactory(PrivateMessageAction.factory(this, privateChatManager)); roomControls.addPlayerActionFactory(SendSoundAction.factory(this, Resources .getString("Chat.send_wakeup"), "wakeUpSound", "phone1.wav")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ roomControls.addPlayerActionFactory(InviteAction.factory(this)); roomControls.addPlayerActionFactory(KickAction.factory(this)); serverStatusControls = new ServerStatusControlsInitializer(serverStatus); playerStatusControls = new SimpleStatusControlsInitializer(this); synchEncoder = new SynchEncoder(this, this); privateChatEncoder = new PrivateChatEncoder(this, privateChatManager); soundEncoder = new SoundEncoder(this); inviteEncoder = new InviteEncoder(this); nameChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { SimplePlayer p = (SimplePlayer) getUserInfo(); p.setName((String) evt.getNewValue()); setUserInfo(p); } }; profileChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { SimplePlayer p = (SimplePlayer) getUserInfo(); SimpleStatus s = (SimpleStatus) p.getStatus(); s = new SimpleStatus(s.isLooking(), s.isAway(), (String) evt .getNewValue(), s.getClient(), s.getIp(), s.getModuleVersion(), s .getCrc()); p.setStatus(s); setUserInfo(p); } }; } public void setConnected(boolean connect) { if (connect) { if (!isConnected()) { try { NodePlayer oldPlayer = me; me = new NodePlayer(playerId); setUserInfo(oldPlayer); initializeConnection(); Command welcomeMessage = welcomer.getWelcomeMessage(); if (welcomeMessage != null) { welcomeMessage.execute(); } registerNewConnection(); } // FIXME: review error message catch (IOException e) { propSupport.firePropertyChange(STATUS, null, Resources.getString( "Chat.unable_to_establish", e.getMessage())); //$NON-NLS-1$ } } } else { if (isConnected()) { closeConnection(); } currentRoom = null; allRooms = new NodeRoom[0]; } propSupport.firePropertyChange(CONNECTED, null, isConnected() ? Boolean.TRUE : Boolean.FALSE); } protected void registerNewConnection() { String path = new SequenceEncoder(moduleName, '/').append(defaultRoomName) .getValue(); send(Protocol.encodeRegisterCommand(me.getId(), path, new PropertiesEncoder(me.toProperties()).getStringValue())); if (GameModule.getGameModule() != null) { String username = (String) GameModule.getGameModule().getPrefs() .getValue("Login"); //$NON-NLS-1$ if (username != null) { send(Protocol.encodeLoginCommand(username)); } } } protected abstract void closeConnection(); protected abstract void initializeConnection() throws IOException; public abstract void send(String command); public void setDefaultRoomName(String defaultRoomName) { this.defaultRoomName = defaultRoomName; } public String getDefaultRoomName() { return defaultRoomName; } public boolean isDefaultRoom(Room r) { return r == null ? false : r.getName().equals(getDefaultRoomName()); } protected void sendStats() { if (isConnected()) { send(Protocol.encodeStatsCommand(new PropertiesEncoder(me.toProperties()) .getStringValue())); } } public void sendToOthers(Command c) { sendToOthers(encoder.encode(c)); } public void sendToAll(String msg) { if (currentRoom != null) { String path = new SequenceEncoder(moduleName, '/').append( currentRoom.getName()).getValue(); forward(path, msg); } } public void forward(String receipientPath, String msg) { if (isConnected() && currentRoom != null && msg != null) { msg = checker.filter(msg, defaultRoomName, currentRoom.getName()); if (msg.length() > compressionLimit) { try { msg = ZIP_HEADER + Base64.encodeBase64String( Compressor.compress(msg.getBytes("UTF-8")) ); } // FIXME: review error message catch (IOException e) { e.printStackTrace(); } } send(Protocol.encodeForwardCommand(receipientPath, msg)); } } public void sendToOthers(String msg) { if (currentRoom != null) { String path = new SequenceEncoder(moduleName, '/').append( currentRoom.getName()).append("~" + me.getId()).getValue(); //$NON-NLS-1$ forward(path, msg); } } public void sendTo(Player recipient, Command c) { String path = new SequenceEncoder(moduleName, '/') .append("*").append(((NodePlayer) recipient).getId()).getValue(); //$NON-NLS-1$ forward(path, encoder.encode(c)); } public void doKick(Player kickee) { send(Protocol.encodeKickCommand(kickee.getId())); } public boolean isKickable(Player kickee) { if (kickee != null) { final Room room = getRoom(); // Is this a locked room? if (room instanceof LockableRoom && ((LockableRoom) room).isLocked()) { if (room instanceof NodeRoom) { // Is the target player in the same room? if (((NodeRoom) room).contains(kickee)) { final String owner = ((NodeRoom) room).getOwner(); // Do I own this room and the target is not me? if (owner != null && owner.equals(getUserInfo().getId()) && !owner.equals(kickee.getId())) { return true; } } } } } return false; } public boolean isInvitable(Player invitee) { if (invitee != null) { final Room room = getRoom(); if (room instanceof NodeRoom) { // Is the target player in a different room? if (!((NodeRoom) room).contains(invitee)) { final String owner = ((NodeRoom) room).getOwner(); // Do I own this room and the target is not me? if (owner != null && owner.equals(getUserInfo().getId()) && !owner.equals(invitee.getId())) { return true; } } } } return false; } /** * Send Invitation to another player to join the current room * * @param invitee * Player to invite */ public void sendInvite(Player invitee) { sendTo(invitee, new InviteCommand(me.getName(), me.getId(), getRoom() .getName())); } /** * Process an invitation request from a player to join a room * * @param player * Inviting player name * @param room * Inviting room */ public void doInvite(String playerId, String roomName) { for (Room room : getAvailableRooms()) { if (room.getName().equals(roomName)) { if (room instanceof NodeRoom) { final String owner = ((NodeRoom) room).getOwner(); if (owner != null && owner.equals(playerId)) { setRoom(room, playerId); return; } } } } } public Room getRoom() { return currentRoom; } public Room[] getAvailableRooms() { return allRooms; } public void addPropertyChangeListener(String propertyName, PropertyChangeListener l) { propSupport.addPropertyChangeListener(propertyName, l); } public void addPropertyChangeListener(PropertyChangeListener l) { propSupport.addPropertyChangeListener(l); } public Player getUserInfo() { return me; } public NodePlayer getMyInfo() { return me; } public void setUserInfo(Player p) { me.setName(p.getName()); me.setStatus(p.getStatus()); sendStats(); propSupport.firePropertyChange(PLAYER_INFO, null, me); } public void lockRoom(LockableRoom r) { if (r instanceof NodeRoom) { final NodeRoom n = (NodeRoom) r; if (n.getOwner().equals(me.getId())) { n.toggleLock(); sendRoomInfo(n); propSupport.firePropertyChange(AVAILABLE_ROOMS, null, allRooms); } } } public void sendRoomInfo(NodeRoom r) { Node dummy = new Node(null, r.getName(), new PropertiesEncoder(r.getInfo()) .getStringValue()); if (isConnected()) { String msg = Protocol.encodeRoomsInfo(new Node[] { dummy }); send(msg); } } public void setRoom(Room r) { setRoom(r, null); } public void setRoom(Room r, String password) { if (isConnected()) { final String newRoom = r.getName(); final String newPath = new SequenceEncoder(moduleName, '/').append( newRoom).getValue(); String msg = Protocol.encodeJoinCommand(newPath, password); send(msg); // Request a synch if we are not the owner if (r instanceof NodeRoom) { final NodeRoom room = (NodeRoom) r; if (newRoom.equals(defaultRoomName)) { GameModule.getGameModule().getGameState().setup(false); } else if (!room.isOwner(me)) { // We are not actually recorded as being in the new room until we get // an update back from // the server. Record a Synch required to the new room. pendingSynchToRoom = room; GameModule.getGameModule().warn( Resources.getString("Chat.synchronize_pending")); } } } } /** * Process a message received from the server * * @param msg * Encoded message */ public void handleMessageFromServer(String msg) { Node n; Properties p; if ((n = Protocol.decodeListCommand(msg)) != null) { Node mod = n.getChild(moduleName); if (mod != null) { updateRooms(mod); } // Rooms have been updated with any new players (including us), so perform // a Synchronize // for a move to a new room if needed. if (pendingSynchToRoom != null) { new SynchAction(pendingSynchToRoom.getOwningPlayer(), this) .actionPerformed(null); pendingSynchToRoom = null; GameModule.getGameModule().warn( Resources.getString("Chat.synchronize_complete")); } } else if ((p = Protocol.decodeRoomsInfo(msg)) != null) { for (int i = 0; i < allRooms.length; ++i) { String infoString = p.getProperty(allRooms[i].getName()); if (infoString != null && infoString.length() > 0) { try { Properties info = new PropertiesEncoder(infoString).getProperties(); allRooms[i].setInfo(info); } // FIXME: review error message catch (IOException e) { e.printStackTrace(); } } } propSupport.firePropertyChange(ROOM, null, currentRoom); propSupport.firePropertyChange(AVAILABLE_ROOMS, null, allRooms); } else if (Protocol.decodeRegisterRequest(msg)) { registerNewConnection(); } else { if (msg.startsWith(ZIP_HEADER)) { try { msg = new String( Compressor.decompress( Base64.decodeBase64( msg.substring(ZIP_HEADER.length()) ) ), "UTF-8" ); } // FIXME: review error message catch (IOException e) { e.printStackTrace(); } } propSupport.firePropertyChange(INCOMING_MSG, null, msg); } } protected void updateRooms(Node module) { Node[] roomNodes = module.getChildren(); NodeRoom[] rooms = new NodeRoom[roomNodes.length]; int defaultRoomIndex = -1; for (int i = 0; i < roomNodes.length; ++i) { Node[] playerNodes = roomNodes[i].getChildren(); NodePlayer[] players = new NodePlayer[playerNodes.length]; boolean containsMe = false; for (int j = 0; j < playerNodes.length; ++j) { players[j] = new NodePlayer(playerNodes[j].getId()); if (players[j].equals(me)) { containsMe = true; } try { Properties p = new PropertiesEncoder(playerNodes[j].getInfo()) .getProperties(); players[j].setInfo(p); if (players[j].equals(me)) { me.setInfo(p); } } // FIXME: review error message catch (IOException e) { e.printStackTrace(); } } rooms[i] = new NodeRoom(roomNodes[i].getId(), players); // Lock room to start with. The ROOM_INFO message will unlock // any rooms that are not locked. Prevents unwanted clients from // connecting while room is in an undefined state. if (!rooms[i].getName().equals(defaultRoomName)) { rooms[i].lock(); } try { if (roomNodes[i].getInfo() != null) { rooms[i].setInfo(new PropertiesEncoder(roomNodes[i].getInfo()) .getProperties()); } } // FIXME: review error message catch (IOException e) { e.printStackTrace(); } if (containsMe) { currentRoom = rooms[i]; } if (defaultRoomName.equals(rooms[i].getName())) { defaultRoomIndex = i; } } if (defaultRoomIndex < 0) { allRooms = ArrayUtils.prepend(rooms, new NodeRoom(defaultRoomName)); } else { allRooms = rooms; NodeRoom swap = allRooms[0]; allRooms[0] = allRooms[defaultRoomIndex]; allRooms[defaultRoomIndex] = swap; } // Do not fire a PropertyChange request, The server will be following // immediately // with a Room List refresh which can cause Icons to flash unexpectedly. // propSupport.firePropertyChange(ROOM, null, currentRoom); // propSupport.firePropertyChange(AVAILABLE_ROOMS, null, allRooms); } public MessageBoard getMessageServer() { return msgSvr; } public Message[] getMessages() { return msgSvr.getMessages(); } public void postMessage(String msg) { msgSvr.postMessage(msg); } public Player stringToPlayer(String s) { NodePlayer p = null; try { PropertiesEncoder propEncoder = new PropertiesEncoder(s); p = new NodePlayer(null); p.setInfo(propEncoder.getProperties()); } // FIXME: review error message catch (IOException e) { e.printStackTrace(); } return p; } public String playerToString(Player p) { Properties props = ((NodePlayer) p).toProperties(); return new PropertiesEncoder(props).getStringValue(); } public void initializeControls(ChatServerControls controls) { playerStatusControls.initializeControls(controls); messageBoardControls.initializeControls(controls); roomControls.initializeControls(controls); serverStatusControls.initializeControls(controls); controls.setRoomControlsVisible(true); final GameModule g = GameModule.getGameModule(); g.addCommandEncoder(synchEncoder); g.addCommandEncoder(privateChatEncoder); g.addCommandEncoder(soundEncoder); g.addCommandEncoder(inviteEncoder); me.setName((String) g.getPrefs().getValue(GameModule.REAL_NAME)); g.getPrefs().getOption(GameModule.REAL_NAME).addPropertyChangeListener( nameChangeListener); SimpleStatus s = (SimpleStatus) me.getStatus(); s = new SimpleStatus(s.isLooking(), s.isAway(), (String) g.getPrefs() .getValue(GameModule.PERSONAL_INFO), Info.getVersion(), s.getIp(), g .getGameVersion() + ((g.getArchiveWriter() == null) ? "" : " (Editing)"), Long .toHexString(g.getCrc())); me.setStatus(s); g.getPrefs().getOption(GameModule.PERSONAL_INFO).addPropertyChangeListener( profileChangeListener); controls.getRoomTree().setCellRenderer(new LockableRoomTreeRenderer()); } public void uninitializeControls(ChatServerControls controls) { messageBoardControls.uninitializeControls(controls); roomControls.uninitializeControls(controls); serverStatusControls.uninitializeControls(controls); playerStatusControls.uninitializeControls(controls); GameModule.getGameModule().removeCommandEncoder(synchEncoder); GameModule.getGameModule().removeCommandEncoder(privateChatEncoder); GameModule.getGameModule().removeCommandEncoder(soundEncoder); GameModule.getGameModule().removeCommandEncoder(inviteEncoder); GameModule.getGameModule().getPrefs().getOption(GameModule.REAL_NAME) .removePropertyChangeListener(nameChangeListener); GameModule.getGameModule().getPrefs().getOption(GameModule.PERSONAL_INFO) .removePropertyChangeListener(profileChangeListener); } }