[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 12.7


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 12.7:

The chat room example from Subsection 12.5.2 can be improved in several ways. First, it would be nice if the participants in the chat room could be identified by name instead of by number. Second, it would be nice if one person could send a private message to another person that would be seen just by that person rather than by everyone. Make these two changes. You can start with a copy of the package netgame.chat. You will also need the package netgame.common, which defines the netgame framework.

To make the first change, you will have to implement a subclass of Hub that can keep track of client names as well as numbers. To get the name of a client to the hub, you can override the extraHandshake() method both in the Hub subclass and in the Client subclass. The extraHandshake() method is called as part of setting up the connection between the client and the hub. It is called after the client has been assigned an ID number but before the connection is considered to be fully established. It should throw an IOException if some error occurs during the setup process. Note that any messages that are sent by the hub as part of the handshake must be read by the client and vice versa. The extraHandshake() method in the Client is defined as:

protected void extraHandshake(ObjectInputStream in, ObjectOutputStream out) 
                                                    throws IOException

while in the Hub, there is an extra parameter that tells the ID number of the client whose connection is being set up:

protected void extraHandshake(in playerID, ObjectInputStream in, 
                              ObjectOutputStream out) throws IOException

In the ChatRoomWindow class, the main() routine asks the user for the name of the computer where the server is running. You can add some code there to ask the user their name. (Just imitate the code that asks for the host name.) You will have to decide what to do if two users want to use the same name.

For the second improvement, personal messages, I suggest writing a new PrivateMessage class. A PrivateMessage object would include both the string that represents the message and the ID numbers of the player to whom the message is being sent and the player who sent the message. The hub will have to be programmed to know how to deal with such messages. A PrivateMessage should only be sent by the hub to the client who is listed as the recipient of the message. You need to decide how the user will input a private message and how the user will select the recipient of the message. Don't forget that PrivateMessage needs to be declared to implement Serializable.

If you attempt this exercise, you are likely to find it quite challenging.


Discussion

My solution defines a subclass, NewChatRoomHub, of Hub to make it possible to manage user names. I also wrote NewChatRoomWindow class, but not as a subclass; instead it is a modified copy of ChatRoomWindow. the Hub class, users are identified by ID numbers. In some ways, it would have been easier to modify Hub, instead of creating a subclass. However, I wanted to build on the package netgame.common, without modifying the existing classes. The NewChatRoomHub class has an instance variable

private TreeMap<Integer,String> nameMap = new TreeMap<Integer,String>();

to keep track of client names by associating each client's name to that client's ID number. That is, nameMap.get(id) is the name of the client whose ID number is id. There has to be a way to add a user name to nameMap when a new client connects, and there has to be a way to remove a client from nameMap when that client disconnects. There also has to be a way for a client to get a name in the first place.

It might have been nice to have a list of authorized users, and give each authorized user a name and a password. The hub would then require a name and a password before it would let a client connect. The list of names and passwords could be stored in a file or, if there are a lot of them, in a database. However, I decided to keep things simple. Clients choose their names when they connect. The client's selected name is sent to the hub as part of the extraHandshake() method. To avoid having duplicate names, the hub will modify the client's name if there is already someone in the chat room using that name. For example, if the client wants to be "Fred" and there is already a Fred, then the hub will assign the name "Fred#2" to the client. Since the hub can modify the name, it sends the client's name back to the client, possibly with some modification. The client reads the returned name as part of the handshake. To implement this idea, the extraHandshake() method in the client simply sends the client's requested name to the hub by writing the name to the connection's output stream. It then reads the possibly modified name from the connection's input stream:

protected void extraHandshake(ObjectInputStream in, ObjectOutputStream out)
                                                              throws IOException {
    try {
        out.writeObject(myName);  // Send user's name request to the server. 
        myName = (String)in.readObject();  // Get the possibly modified name.
    }
    catch (Exception e) {
        throw new IOException("Error while setting up connection: " + e);
    }
}

(This code is from the ChatClient class, which is a nested subclass of NewChatRoomWindow.)

The corresponding extraHandshake() method in the NewChatRoomHub class has to read the name that was sent by the client, possibly modify the name, and write the name back to the client. It also adds the client to nameMap, which contains the names of all connected clients. My code is very careful to produce a name that is non-null, non-empty, and unique:

protected void extraHandshake(int playerID, 
                  ObjectInputStream in, ObjectOutputStream out) throws IOException {
    try {
        String name = (String)in.readObject(); // Read requested name from client.
        if (name == null)
            name = "noname";
        if (name.length() > 15)
            name = name.substring(0,15).trim();
        if (name.equals(""))
            name = "noname";
        synchronized(nameMap) {
               // Synchronized to be absolutely sure that there will be
               // no duplicate names.
            if (nameMap.containsValue(name)) {
                String approvedName = name;
                int num = 2;
                while (nameMap.containsValue(approvedName)) {
                    approvedName = name + "#" + num;
                    num++;
                }
                name = approvedName;
            }
        }
        out.writeObject(name);      // Send actual name to client.
        nameMap.put(playerID,name); // Add client's name to nameMap.
    }
    catch (Exception e) {
        throw new IOException("Error while setting up connection: " + e);
    }
}

In order to get private messages from one player to another, I defined a PrivateMessage class, as suggested in the exercise. An object pm of type PrivateMessage has public instance variables pm.senderID and pm.recipientID to hold the ID numbers of the sender and the recipient, and it has pm.message to hold the message itself. The hub has to know what to do with such a message. Messages are processed in the hub by the messageReceived() method. I overrode this method in NewChatRoomHub to handle PrivateMessage and to pass messages of other types on to the superclass. One interesting point is that the hub sets the senderID in the message to be the ID number of the client that actually sent the message. This is done to prevent clients from sending forged messages that appear to be from someone else. Here is the messageReceived() method from NewChatRoomHub:

/**
 * This method is overridden to provide support for PrivateMessages.
 * If a PrivateMessage is received from some client, this method
 * will set the senderID field in the message to be the ID number
 * of the client who sent the message.  It will then send the
 * message on to the specified recipient.  If some other type
 * of message is received, it is handled by the messageReceived()
 * method in the superclass (which will wrap it in a ForwardedMessage
 * and send it to all connected clients).
 */
protected void messageReceived(int playerID, Object message) {
    if (message instanceof PrivateMessage) {
        PrivateMessage pm = (PrivateMessage)message;
        pm.senderID = playerID;
        sendToOne(pm.recipientID, pm);
    }
    else
        super.messageReceived(playerID, message);
}

It is easy enough to add some extra inputs to the original window from ChatRoomWindow to make it possible for the user to send private messages. Just add a text input box for the message, a button to send the message, and a ComboBox that contains a list of possible recipients. (The ComboBox control has not yet been covered in this book, but see Subsection 13.3.3.) The user enters a message, selects the recipient from the list, and clicks the button to send. The program constructs a PrivateMessage containing the message and the ID number of the recipient, and it sends the PrivateMessage to the hub, which forwards it on to the intended recipient. This code can be found in NewChatRoomWindow.

At least, it would be easy if not for the fact that I want users of the chat room to be identified by name, not ID number. The whole point of using names is that they provide a more meaningful way to refer to users. Each client already gets a list of ID numbers of all connected clients, but I want each client to have a list of names of connected clients. The hub has that information. I just needed a way to get that information to the clients. Since the client list changes every time a client connects or disconnects, the hub will have to send new information to the clients every time that happens. I was able to program this behavior by overriding the playerConnected() and playerDisconnected() methods from the Hub class. My NewChatRoomHub class includes the following definitions for those methods (which do nothing in class Hub):

/**
 *  This method is called when a new client connects.  It is called
 *  after the extraHandshake() method has been called, so that the
 *  client's name has already been added to nameMap.  This method
 *  creates a ClientConnectedMessage and sends it to all connected
 *  clients to announce the new participant in the chat room.
 */
protected void playerConnected(int playerID) {
    resetOutput(); // Reset the output stream before resending nameMap.
    sendToAll(new ClientConnectedMessage(playerID,nameMap));
}

/**
 * This method is called when a client has been disconnected from
 * this hub.  It removes the client from the nameMap and sends
 * a ClientDisconnectMessage to all connected players to
 * announce the fact that the client has left the chat room. 
 */
protected void playerDisconnected(int playerID) {
    String name = nameMap.get(playerID); // Get the departing player's name.
    nameMap.remove(playerID);  // Remove the player from nameMap.
    resetOutput(); // Reset the output stream before resending nameMap.
    sendToAll(new ClientDisconnectedMessage(playerID, name, nameMap));
}

It actually took me quite a while to get this right. To simplify the processing, I ended up defining new message types, ClientConnectedMessage and ClientDisconnectedMessage, to hold the information that I wanted to send to the clients. A ClientConnectedMessage includes the ID number of the client who has just connected, as well as the nameMap that contains the ID numbers and names of all connected clients. When a client receives this message, it can inform the user that someone has entered the chat room, and it can use the nameMap to construct the list of names of possible recipients of private messages. ClientDisconnectedMessages are similar.

Not for the first time, I introduced a bug by forgetting that an ObjectOuputStream has to be reset if it is used to send the same object twice, with modifications between the two transmissions. This is necessary in this example because the same object, nameMap, is transmitted as part of every ClientConnectedMessage and every ClientDisconnectedMessage. When I didn't reset the output, the changes that I made to that object were not seen by the clients that received those messages. In my code, the method resetOuput() will cause the output stream leading to every client to be reset.


Back in the NewChatRoomWindow class, the chat room client has to be prepared to receive PrivateMessages, ClientConnectedMessages, and ClientDisconnectedMessages. It can also receive ForwardedMessages which are used, as in the original chat room application, to broadcast a message from one client to all connected clients. In each case, the client will add an appropriate message to the transcript. It uses clientNameMap—the local copy of nameMap—to translate ID numbers from the messages into the names of the corresponding chat room users:

protected void messageReceived(Object message) {
    if (message instanceof ForwardedMessage) {
        ForwardedMessage fm = (ForwardedMessage)message;
        String senderName = clientNameMap.get(fm.senderID);
        addToTranscript(senderName + " SAYS:  " + fm.message);
    }
    else if (message instanceof PrivateMessage) {
        PrivateMessage pm = (PrivateMessage)message;
        String senderName = clientNameMap.get(pm.senderID);
        addToTranscript("PRIVATE MESSAGE FROM " + senderName + ":  " + pm.message);
    }
    else if (message instanceof ClientConnectedMessage) {
        ClientConnectedMessage cm = (ClientConnectedMessage)message;
        addToTranscript('"' + cm.nameMap.get(cm.newClientID) + "\" HAS JOINED THE CHAT ROOM.");
        newNameMap(cm.nameMap);
    }
    else if (message instanceof ClientDisconnectedMessage) {
        ClientDisconnectedMessage dm = (ClientDisconnectedMessage)message;
        addToTranscript('"' + clientNameMap.get(dm.departingClientID) + "\" HAS LEFT THE CHAT ROOM.");
        newNameMap(dm.nameMap);
    }
}

The newNameMap() method, which is called twice in the above code, will assign the nameMap from the message to clientNameMap. It also updates the ComboBox, , which stores the list of names of connected users (leaving out the name of the user who is using the program, since he won't want to send a private message to himself). If the user who is currently selected is still in the new list, then that user is selected again.

private void newNameMap(final TreeMap<Integer,String> nameMap) {
    Platform.runLater( () ->  {
        clientNameMap = nameMap;
        String currentlySelected = clientList.getSelectionModel().getSelectedItem();
        clientList.getItems().clear();
        boolean someoneIsThere = false;
        boolean currentSelectionIsThere = false;
        for (String str: nameMap.values()) {
            if (!str.equals(myName)) {
                clientList.getItems().add(str);
                someoneIsThere = true;
            }
            if (str.equals(currentlySelected))
                currentSelectionIsThere = true;
        }
        privateMessageInput.setEditable(someoneIsThere);
        privateMessageInput.setDisable(!someoneIsThere);
        sendPrivateButton.setDisable(!someoneIsThere);
        if (!someoneIsThere)
            clientList.getItems().add("(no one available)");
        if (currentSelectionIsThere)
            clientList.getSelectionModel().select(currentlySelected);
        else
            clientList.getSelectionModel().select(0);
    });
}

The complete source code for the new chat room application is shown below. It consists of six classes, which I defined in a package named netgame.newchat. These classes depend on the classes from the netgame.common package.


The Solution

NewChatRoomServer is a short class with a main routine that simply creates a hub that acts as the server for the application:

package netgame.newchat;

import java.io.IOException;

/**
 * This class contains just a small main class that creates a NewChatRoomHub
 * and starts it listening on port 37830.  This port is used
 * by the NewChatRoomWindow application.  This program should be run
 * on the computer that "hosts" the chat room.  See the NewChatRoomWindow
 * class for more details.  Once the server starts listening, it
 * will listens for connection requests from clients until the
 * NewChatRoomServer program is terminated (for example by a 
 * Control-C).
 */
public class NewChatRoomServer {

    private final static int PORT = 37830;
    
    public static void main(String[] args) {
        try {
            new NewChatRoomHub(PORT);
        }
        catch (IOException e) {
            System.out.println("Can't create listening socket.  Shutting down.");
        }
    }
    
}



There are three short classes that define various types of messages which are used in the application:

package netgame.newchat;

import java.io.Serializable;

/**
 * Represents a string sent as a message from one client
 * to another client.  Note:  The ChatRoomHub will set the
 * senderID of a PrivateMessage to be the ID number of the
 * client who actually sent the message, in order to avoid
 * the possibility of rogue clients that try to forge 
 * messages that appear to come from other clients.
 */
public class PrivateMessage implements Serializable {
    
    public int senderID;    // The ID number of the sender.
    public int recipientID; // The ID number of the recipient.
    public String message;  // The message.

    /**
     *  Create a private message from one user to another.
     *  The senderID of the message will be set by the hub.
     */
    public PrivateMessage(int recipientID, String message) {
        this.recipientID = recipientID;
        this.message = message;
    }

}

 

package netgame.newchat;

import java.io.Serializable;
import java.util.TreeMap;

/**
 * A message of this type will be sent by the hub to all
 * connected clients when a new client joins the chat room
 */
public class ClientConnectedMessage implements Serializable {
    
    public int newClientID;  // The ID number of the client who has connected.
    public TreeMap<Integer,String> nameMap;  // Map of all connected client IDs to their names.
    
    public ClientConnectedMessage(int newClientID, TreeMap<Integer,String> nameMap) {
        this.newClientID = newClientID;
        this.nameMap = nameMap;
    }

}



package netgame.newchat;

import java.io.Serializable;
import java.util.TreeMap;

/**
 * A message of this type will be sent by the hub to all
 * remaining connected clients when a client leaves the
 * chat room.
 */
public class ClientDisconnectedMessage implements Serializable {
    
    public int departingClientID;  // The ID number of the client who has left the chat room.
    public String departingClientName;  // The name of the departing client
    public TreeMap<Integer,String> nameMap;  // Map of all connected client IDs to their names.
                                             //  (Note that the departing client is not included.)

    public ClientDisconnectedMessage(int departingClientID,
            String departingClientName, TreeMap<Integer,String> nameMap) {
        this.departingClientID = departingClientID;
        this.departingClientName = departingClientName;
        this.nameMap = nameMap;
    }
    

}


The subclass of Hub that defines the server for the application:

package netgame.newchat;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.TreeMap;

import netgame.common.*;

/**
 *  This class defines the "hub" that acts as a server for the
 *  chat room application.  It extends the basic Hub class in
 *  order to support names for clients, as well as ID numbers.
 */
public class NewChatRoomHub extends Hub {
    
    /**
     * This map keeps track of the names of all connected clients.
     * It maps client ID numbers to client names.
     */
    private TreeMap<Integer,String> nameMap = new TreeMap<Integer,String>();

    /**
     * Create a NewChatRoomHub, which will listen for connections on
     * a specified port.
     * @param port the port on which to listen for connections
     * @throws IOException if it is not possible to create a listening socket
     */
    public NewChatRoomHub(int port) throws IOException {
        super(port);
    }

    /**
     * This method is called as part of the connection setup between this hub
     * and a client that has requested a connection.  It is overridden in this
     * class so that a name can be assigned to the client as part of the setup
     * process.  This method works in cooperation with the extraHandshake()
     * method in the client class (which is defined as a nested class inside
     * NewChatRoomWindow).  In this method, the Hub reads a string from the
     * user that contains the name that the client wants to use.  The name
     * can be modified to make sure that it is non-null, 15 characters or less.
     * The resulting name is further modified by adding a suffix such as
     * "#2" or "#3" if the name is already in use by another client.  Finally,
     * the possibly modified name is sent back to the client, which will use
     * the returned value as the name that identifies the client in the chat
     * room.
     */
    protected void extraHandshake(int playerID, 
                      ObjectInputStream in, ObjectOutputStream out) throws IOException {
        try {
            String name = (String)in.readObject();
            if (name == null)
                name = "noname";
            if (name.length() > 15)
                name = name.substring(0,15).trim();
            if (name.equals(""))
                name = "noname";
            synchronized(nameMap) {
                if (nameMap.containsValue(name)) {
                    String approvedName = name;
                    int num = 2;
                    while (nameMap.containsValue(approvedName)) {
                        approvedName = name + "#" + num;
                        num++;
                    }
                    name = approvedName;
                }
            }
            out.writeObject(name);
            nameMap.put(playerID,name);
        }
        catch (Exception e) {
            throw new IOException("Error while setting up connection: " + e);
        }
    }

    /**
     * This method is overridden to provide support for PrivateMessages.
     * If a PrivateMessage is received from some client, this method
     * will set the senderID field in the message to be the ID number
     * of the client who sent the message.  It will then send the
     * message on to the specified recipient.  If some other type
     * of message is received, it is handled by the messageReceived()
     * method in the superclass (which will wrap it in a ForwardedMessage
     * and send it to all connected clients).
     */
    protected void messageReceived(int playerID, Object message) {
        if (message instanceof PrivateMessage) {
            PrivateMessage pm = (PrivateMessage)message;
            pm.senderID = playerID;
            sendToOne(pm.recipientID, pm);
        }
        else
            super.messageReceived(playerID, message);
    }

    /**
     *  This method is called when a new client connects.  It is called
     *  after the extraHandshake() method has been called, so that the
     *  client's name has already been added to nameMap.  This method
     *  creates a ClientConnectedMessage and sends it to all connected
     *  clients to announce the new participant in the chat room.
     */
    protected void playerConnected(int playerID) {
        resetOutput(); // Reset the output stream before resending nameMap.
        sendToAll(new ClientConnectedMessage(playerID,nameMap));
    }

    /**
     * This method is called when a client has been disconnected from
     * this hub.  It removes the client from the nameMap and sends
     * a ClientDisconnectedMessage to all connected players to
     * announce the fact that the client has left the chat room. 
     */
    protected void playerDisconnected(int playerID) {
        String name = nameMap.get(playerID); // Get the departing player's name.
        nameMap.remove(playerID);  // Remove the player from nameMap.
        resetOutput(); // Reset the output stream before resending nameMap.
        sendToAll(new ClientDisconnectedMessage(playerID, name, nameMap));
    }
    
}



And finally, the class that defines the clients, NewChatRoomWindow. This class is a modified version of ChatRoomWindow from the original chat room application. Significant changes from the original are shown in red.

package netgame.newchat;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.control.TextArea;
import javafx.scene.control.Alert;
import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.layout.Priority;
import javafx.geometry.Insets;

import java.util.Optional;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.TreeMap;

import netgame.common.*;

/**
 * This class represents a client for a "chat room" application.  The chat
 * room is hosted by a server running on some computer.  The user of this
 * program must know the host name (or IP address) of the computer that
 * hosts the chat room.  When this program is run, it asks for that
 * information and for the name that the user wants to use in the chat
 * room.  Then, it opens a window that has an input box where the
 * user can enter messages to be sent to the chat room.  The message is 
 * sent when the user presses return in the input box or when the
 * user clicks a Send button.  There is also a text area that shows 
 * a transcript of all messages from participants in the chat room.
 * <p>The user can also send private messages to individual users.
 * The user selects the recipient's name from a pop-up list of
 * connected users.
 * <p>Participants in the chat room are represented by ID numbers
 * that are assigned to them by the server when they connect. They
 * also have names which they select.
 */
public class NewChatRoomWindow extends Application {
    
    public static void main(String[] args) {
        launch(args);
    }
    //---------------------------------------------------------------------------------
    
    private final static int PORT = 37830; // The ChatRoom port number; can't be 
                                           // changed here unless the ChatRoomServer
                                           // program is also changed.

    private TextField messageInput;   // For entering messages to be sent to the chat room
    private Button sendButton;        // Sends the contents of the messageInput.
    private Button quitButton;        // Leaves the chat room cleanly, by sending a DisconnectMessage
    
    private TextArea transcript;      // Contains all messages sent by chat room participant, as well
                                      //    as a few additional status messages, 
                                      //    such as when a new user arrives.
    
    private ChatClient connection;    // Represents the connection to the Hub; used to send messages;
                                      // also receives and processes messages from the Hub.
    
    private volatile boolean connected; // This is true while the client is connected to the hub.
    
    private volatile String myName; // The name that this client uses in the chat room.
                                    // Originally selected by the user, but might be modified
                                    // if there is already a client of the same name connected
                                    // to the Hub.

    private volatile TreeMap<Integer,String> clientNameMap = new TreeMap<Integer, String>();
                                    // The clientNameMap maps client ID numbers to the names that they are
                                    // using in the chat room.  Every time a client connects or disconnects,
                                    // the Hub sends a new, modified name map to each connected client.  When
                                    // that message is received, the clientNameMap is replaced with the new value,
                                    // and the content of the clientList is replaced with info from the nameMap.

    private ComboBox<String> clientList;    // List of connected client names, where the user can select
                                            //   the client who is to receive the private message.

    private TextField privateMessageInput;  // For entering messages to be set to individual clients.
    private Button sendPrivateButton;   // Sends the contents of privateMesssageInput to the user selected
                                        //   in the clientList.

    /**
     * Gets the host name (or IP address) of the chat room server from the
     * user and then opens the main window.  The program ends when the user
     * closes the window.
     */
    public void start( Stage stage ) {
        
        TextInputDialog question = new TextInputDialog();
        question.setHeaderText("Enter the host name of the\ncomputer that hosts the chat room.");
        question.setContentText("Host Name:");
        Optional<String> response = question.showAndWait();
        if ( ! response.isPresent() )
            System.exit(0);
        String host = response.get().trim();
        if (host == null || host.trim().length() == 0)
            System.exit(0);
        
        question = new TextInputDialog();
        question.setHeaderText("Enter the name that you want\nto use in the chat room.");
        question.setContentText("Your Name:");
        response = question.showAndWait();
        if ( ! response.isPresent() )
            System.exit(0);
        myName = response.get().trim();
        if (myName == null || myName.trim().length() == 0)
            System.exit(0);

        transcript = new TextArea();
        transcript.setPrefRowCount(30);
        transcript.setPrefColumnCount(60);
        transcript.setWrapText(true);
        transcript.setEditable(false);

        sendButton = new Button("send to all");
        quitButton = new Button("quit");
        messageInput = new TextField();
        messageInput.setPrefColumnCount(40);
        sendButton.setOnAction( e -> doSend() );
        quitButton.setOnAction( e -> doQuit() );
        sendButton.setDisable(true);
        messageInput.setEditable(false);
        messageInput.setDisable(true);
        
        sendPrivateButton = new Button("send to one");
        sendPrivateButton.setOnAction( e -> doSendPrivateMessage() );
        privateMessageInput = new TextField();
        privateMessageInput.setPrefColumnCount(30);
        clientList = new ComboBox<String>();
        clientList.setEditable(false);
        clientList.getItems().add("(no one available)");
        clientList.getSelectionModel().select(0);
        
        HBox bottomRow1 = new HBox(8, new Label("YOU SAY:"), messageInput, sendButton, quitButton);
        HBox.setHgrow(messageInput, Priority.ALWAYS);
        HBox.setMargin(quitButton, new Insets(0,0,0,50));
        
        HBox bottomRow2 = new HBox(8, new Label("SAY:"), privateMessageInput, 
                                       new Label(" To: "), clientList, sendPrivateButton);
        HBox.setHgrow(privateMessageInput, Priority.ALWAYS);
        
        VBox bottom = new VBox(8, bottomRow1, bottomRow2);
        bottom.setPadding(new Insets(8));
        bottom.setStyle("-fx-border-color: black; -fx-border-width:2px");
        BorderPane root = new BorderPane(transcript);
        root.setBottom(bottom);
        
        stage.setScene( new Scene(root) );
        stage.setTitle("Networked Chat");
        stage.setResizable(false);
        stage.setOnHidden( e -> doQuit() );
        stage.show();
        
        /* The next two lines make the sendButton and sendPrivateButton into the
         * default button for the window exactly when the corresponding input box
         * is focussed.  This means that the user can just hit return while 
         * typing in an input box to send the message. */
        
        messageInput.focusedProperty().addListener( 
                        (target,oldVal,newVal) -> sendButton.setDefaultButton(newVal) );
        privateMessageInput.focusedProperty().addListener( 
                         (target,oldVal,newVal) -> sendPrivateButton.setDefaultButton(newVal) );
        
        new Thread() {
                // This is a thread that opens the connection to the server.  Since
                // that operation can block, it's not done directly in the constructor.
                // Once the connection is established, the user interface elements are
                // enabled so the user can send messages.  The Thread dies after
                // the connection is established or after an error occurs.
            public void run() {
                try {
                    addToTranscript("Connecting to " + host + " ...");
                    connection = new ChatClient(host);
                    connected = true;
                    Platform.runLater( () -> {
                        messageInput.setEditable(true);
                        messageInput.setDisable(false);
                        sendButton.setDisable(false);
                        messageInput.requestFocus();
                    });
                }
                catch (IOException e) {
                    Platform.runLater( () -> {
                        addToTranscript("Connection attempt failed.");
                        addToTranscript("Error: " + e);
                    });
                }
            }
        }.start();

    }
    


    /**
     * A ChatClient connects to the Hub and is used to send messages to
     * and receive messages from a Hub.  Four types of message are
     * received from the Hub.  A ForwardedMessage represents a message
     * that was entered by some user and sent to all users of the
     * chat room.  A PrivateMessage represents a message that was
     * sent by another user only to this user.  A ClientConnectedMessage
     * is sent when a new user enters the room.  A ClientDisconnectedMessage
     * is sent when a user leaves the room.
     */
    private class ChatClient extends Client {
        
        /**
         * Opens a connection the chat room server on a specified computer.
         */
        ChatClient(String host) throws IOException {
            super(host, PORT);
        }
        
        /**
         * Responds when a message is received from the server.
         */
        protected void messageReceived(Object message) {
            if (message instanceof ForwardedMessage) {
                ForwardedMessage fm = (ForwardedMessage)message;
                String senderName = clientNameMap.get(fm.senderID);
                addToTranscript(senderName + " SAYS:  " + fm.message);
            }
            else if (message instanceof PrivateMessage) {
                PrivateMessage pm = (PrivateMessage)message;
                String senderName = clientNameMap.get(pm.senderID);
                addToTranscript("PRIVATE MESSAGE FROM " + senderName + ":  " + pm.message);
            }
            else if (message instanceof ClientConnectedMessage) {
                ClientConnectedMessage cm = (ClientConnectedMessage)message;
                addToTranscript('"' + cm.nameMap.get(cm.newClientID) + "\" HAS JOINED THE CHAT ROOM.");
                newNameMap(cm.nameMap);
            }
            else if (message instanceof ClientDisconnectedMessage) {
                ClientDisconnectedMessage dm = (ClientDisconnectedMessage)message;
                addToTranscript('"' + clientNameMap.get(dm.departingClientID) + "\" HAS LEFT THE CHAT ROOM.");
                newNameMap(dm.nameMap);
            }
        }
        
        /**
         * This method is part of the connection set up.  It sends the user's selected
         * name to the hub by writing that name to the output stream.  The hub will
         * respond by sending the name back to this client, possibly modified if someone
         * is the chat room is already using the selected name.
         */
        protected void extraHandshake(ObjectInputStream in, ObjectOutputStream out) throws IOException {
            try {
                out.writeObject(myName);  // Send user's name request to the server. 
                myName = (String)in.readObject();  // Get the actual name from the server.
            }
            catch (Exception e) {
                throw new IOException("Error while setting up connection: " + e);
            }
        }

        /**
         * Called when the connection to the client is shut down because of some
         * error message.  (This will happen if the server program is terminated.)
         */
        protected void connectionClosedByError(String message) {
            addToTranscript("Sorry, communication has shut down due to an error:\n     " + message);
            Platform.runLater( () -> {
	            sendButton.setDisable(true);
	            messageInput.setDisable(true);
	            messageInput.setEditable(false);
	            messageInput.setText("");
	            sendPrivateButton.setDisable(true);
	            privateMessageInput.setDisable(true);
	            privateMessageInput.setEditable(false);
            });
            connected = false;
            connection = null;
        }
        
        // Note:  the methods playerConnected() and playerDisconnected(), which where present here
        // in ChatRoomWindow, were removed, since their functionality (to announce arrivals
        // and departures) has been taken over by ClientConnectedMessage and ClientDisconnectedMessage.

    } // end nested class ChatClient
    
  
    /**
     * Adds a string to the transcript area, followed by a blank line.
     */
    private void addToTranscript(String message) {
        Platform.runLater( () -> transcript.appendText(message + "\n\n") );
    }
    
    
    /**
     * Called when the user clicks the Quit button or closes
     * the window by clicking its close box. Called from the
     * application thread.
     */
    private void doQuit() {
        if (connected)
            connection.disconnect();  // Sends a DisconnectMessage to the server.
        try {
            Thread.sleep(500); // Time for DisconnectMessage to actually be sent.
        }
        catch (InterruptedException e) {
        }
        System.exit(0);
    }

    /**
     * This method is called when a ClientConnectedMessage or ClientDisconnectedMessage
     * is received from the hub.  Its job is to save the nameMap that is part of the
     * message and use it to rebuild the contents of the ComboBox, clientList, where
     * the user selects the recipient of a private message.  It also enables or
     * disables the private message input box and send button, depending on whether
     * there are any possible message recipients.
     * @param nameMap the new nameMap, which will replace the value of clientNameMap.
     */
    private void newNameMap(final TreeMap<Integer,String> nameMap) {
        Platform.runLater( () ->  {
            clientNameMap = nameMap;
            String currentlySelected = clientList.getSelectionModel().getSelectedItem();
            clientList.getItems().clear();
            boolean someoneIsThere = false;
            boolean currentSelectionIsThere = false;
            for (String str: nameMap.values()) {
                if (!str.equals(myName)) {
                    clientList.getItems().add(str);
                    someoneIsThere = true;
                }
                if (str.equals(currentlySelected))
                    currentSelectionIsThere = true;
            }
            privateMessageInput.setEditable(someoneIsThere);
            privateMessageInput.setDisable(!someoneIsThere);
            sendPrivateButton.setDisable(!someoneIsThere);
            if (!someoneIsThere)
                clientList.getItems().add("(no one available)");
            if (currentSelectionIsThere)
                clientList.getSelectionModel().select(currentlySelected);
            else
                clientList.getSelectionModel().select(0);
        });
    }
    

    /** 
     * Send the string entered by the user as a message
     * to the Hub, using the ChatClient that handles communication
     * for this ChatRoomWindow.  Note that the string is not added
     * to the transcript here.  It will get added after the Hub
     * receives the message and broadcasts it to all clients,
     * including this one.  Called from the application thread.
     */
    private void doSend() {
        String message = messageInput.getText();
        if (message.trim().length() == 0)
            return;
        connection.send(message);
        messageInput.selectAll();
        messageInput.requestFocus();
    }

    
    private void doSendPrivateMessage() {
        // Send a private message to a specified recipient.
        // If the private message inputbox is empty, nothing is done.
        String message = privateMessageInput.getText();
        if (message.trim().length() == 0)
            return;
        String recipient = clientList.getSelectionModel().getSelectedItem(); // name of recipient.
        int recipientID = -1;  // The ID number of the recipient
        for (int id : clientNameMap.keySet()) {
            // Search the clientNameMap to find the ID number
            // corresponding to the specified recipient name.
            if (recipient.equals(clientNameMap.get(id))) {
                recipientID = id;
                break;
            }
        }
        if (recipientID == -1) {
            Alert alert = new Alert(Alert.AlertType.ERROR,
                    "Funny... The selected recipient\ndoesn't seem to exit???");
            alert.showAndWait();
            return;
        }
        connection.send(new PrivateMessage(recipientID,message));
        addToTranscript("Sent to " + recipient + ":  " + message);
    }
    

} // end class NewChatRoomWindow


[ Exercises | Chapter Index | Main Index ]