import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.io.*; import java.net.*; import java.util.ArrayList; /** * Opens a window that can be used for a two-way network chat. A BuddyChat window * is opened by the BuddyChat program when either an incoming connection request * arrives or the user wants to send an outgoing request. This class is meant for * use only with BuddyChat. It does not have a main() program of its own; the * user must run BuddyChat. *

Protocol used for network connection: As soon as the connection has been * opened, the client (the side that requested the conection) sends two lines of * text to the server (the side that received the request). The first line is * a "password" or "secret" that was created by and provided by the BuddyChatServer * as a way for the client to prove that it got its connection information from * the BuddyChatServer. The second line is the client's "handle". (The handle * is not verified in any way; it is just reported to the user.) */ public class BuddyChatWindow extends JFrame { /** * Possible states of the thread that handles the network connection. */ private enum ConnectionState { CONNECTING, CONNECTED, CLOSED }; /** * Used to keep track of where on the screen the previous window * was opened, so that the next window can be placed at a * different position. */ private static Point previousWindowLocation; /** * The thread that handles the connection; defined by a nested class. */ private ConnectionHandler connection; /** * Control buttons that appear in the window. */ private JButton closeButton, clearButton, saveButton, sendButton; /** * Input box for messages that will be sent to the other side of the * network connection. */ private JTextField messageInput; /** * Contains a transcript of messages sent and received, along with * information about the progress and state of the connection. */ private JTextArea transcript; /** * A list of all open BuddyChatWindows. */ private static ArrayList openWindows = new ArrayList(); /** * To be called by BuddyChatServer when the user ends the program by * clicking on the "Close All Windows and Quit" button. */ public static void closeAll() { Object[] windows = openWindows.toArray(); for (int i = 0; i < windows.length; i++) ((JFrame)windows[i]).dispose(); } /** * Returns the number of currently open BuddyChatWindows. */ public static int openWindowCount() { return openWindows.size(); } /** * Open a window to communicate over a socket that has already been created (by the * BuddyChat program). No data has yet been sent over the Socket. The "secret" is * a password known to the the BuddyChat program that must be sent BY the remote client * TO this window. This represents an incoming connection request. */ public BuddyChatWindow(Socket connectedSocket, String secret) { super("Connection Request Received"); create(); connection = new ConnectionHandler(connectedSocket,secret); } /** * Create a window that will open a connection to a specified machine. This is done * by the BuddyChat program when the user wants to open a connection to one of the * people in the buddy list. This represents an outgoing connection request. * @param hostName The host for use in opening the socket. * @param port The port for use in opening the socket. * @param myName The user's "handle" that identifies the user in the buddy list. * @param partnerName The "handle" of the buddy list entry to which the user wants to connect. * @param secret The remote user's password, which must be sent BY this window TO the remote client. */ public BuddyChatWindow(String hostName, int port, String myName, String partnerName, String secret) { super("Chatting with " + partnerName); create(); connection = new ConnectionHandler(hostName,port,myName,partnerName,secret); } /** * Set up the window and make it visible on the screen. This method is called * by both constructors. */ private void create() { ActionListener actionHandler = new ActionHandler(); closeButton = new JButton("Close"); closeButton.addActionListener(actionHandler); clearButton = new JButton("Clear"); clearButton.addActionListener(actionHandler); sendButton = new JButton("Send"); sendButton.addActionListener(actionHandler); sendButton.setEnabled(false); saveButton = new JButton("Save Transcript"); saveButton.addActionListener(actionHandler); messageInput = new JTextField(); messageInput.addActionListener(actionHandler); messageInput.setEditable(false); transcript = new JTextArea(20,60); transcript.setLineWrap(true); transcript.setWrapStyleWord(true); transcript.setEditable(false); JPanel content = new JPanel(); content.setLayout(new BorderLayout(3,3)); content.setBackground(Color.GRAY); JPanel buttonBar = new JPanel(); buttonBar.setLayout(new GridLayout(1,3,3,3)); buttonBar.setBackground(Color.GRAY); JPanel inputBar = new JPanel(); inputBar.setLayout(new BorderLayout(3,3)); inputBar.setBackground(Color.GRAY); content.setBorder(BorderFactory.createLineBorder(Color.GRAY, 3)); content.add(buttonBar, BorderLayout.NORTH); content.add(inputBar, BorderLayout.SOUTH); content.add(new JScrollPane(transcript), BorderLayout.CENTER); buttonBar.add(saveButton); buttonBar.add(clearButton); buttonBar.add(closeButton); inputBar.add(new JLabel("Your Message:"), BorderLayout.WEST); inputBar.add(messageInput, BorderLayout.CENTER); inputBar.add(sendButton, BorderLayout.EAST); setContentPane(content); pack(); if (previousWindowLocation == null) previousWindowLocation = new Point(40,80); else { Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); previousWindowLocation.x += 50; if (previousWindowLocation.x + getWidth() > screenSize.width) previousWindowLocation.x = 10; previousWindowLocation.y += 30; if (previousWindowLocation.y + getHeight() > screenSize.height) previousWindowLocation.y = 50; } setLocation(previousWindowLocation); setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); openWindows.add(this); addWindowListener( new WindowAdapter() { public void windowClosed(WindowEvent evt) { if (connection != null && connection.getConnectionState() != ConnectionState.CLOSED) { connection.close(); } openWindows.remove(this); if (openWindows.size() == 0 && !BuddyChat.isRunning()) { try { Thread.sleep(1000); } catch (InterruptedException e) { } System.exit(0); } } }); setVisible(true); } // end constructor /** * Defines responsed to buttons, and when the user presses return in the message input box. */ private class ActionHandler implements ActionListener { public void actionPerformed(ActionEvent evt) { Object source = evt.getSource(); if (source == closeButton) { dispose(); } else if (source == clearButton) { transcript.setText(""); } else if (source == saveButton) { doSave(); } else if (source == sendButton || source == messageInput) { if (connection != null && connection.getConnectionState() == ConnectionState.CONNECTED) { connection.send(messageInput.getText()); messageInput.selectAll(); messageInput.requestFocus(); } } } } /** * Save the contents of the transcript area to a file selected by the user. */ private void doSave() { JFileChooser fileDialog = new JFileChooser(); File selectedFile; //Initially selected file name in the dialog. selectedFile = new File("transcript.txt"); fileDialog.setSelectedFile(selectedFile); fileDialog.setDialogTitle("Select File to be Saved"); int option = fileDialog.showSaveDialog(this); if (option != JFileChooser.APPROVE_OPTION) return; // User canceled or clicked the dialog's close box. selectedFile = fileDialog.getSelectedFile(); if (selectedFile.exists()) { // Ask the user whether to replace the file. int response = JOptionPane.showConfirmDialog( this, "The file \"" + selectedFile.getName() + "\" already exists.\nDo you want to replace it?", "Confirm Save", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE ); if (response == JOptionPane.NO_OPTION) return; // User does not want to replace the file. } PrintWriter out; try { FileWriter stream = new FileWriter(selectedFile); out = new PrintWriter( stream ); } catch (Exception e) { JOptionPane.showMessageDialog(this, "Sorry, but an error occurred while trying to open the file:\n" + e); return; } try { out.print(transcript.getText()); // Write text from the TextArea to the file. out.close(); if (out.checkError()) // (need to check for errors in PrintWriter) throw new IOException("Error check failed."); } catch (Exception e) { JOptionPane.showMessageDialog(this, "Sorry, but an error occurred while trying to write the text:\n" + e); } } /** * Add a line of text to the transcript area. * @param message text to be added; a line feed is added at the end. */ private void postMessage(String message) { transcript.append(message + "\n"); // The following line is a nasty kludge that was the only way I could find to force // the transcript to scroll so that the text that was just added is visible in // the window. Without this, text can be added below the bottom of the visible area // of the transcript. transcript.setCaretPosition(transcript.getDocument().getLength()); } /** * Defines the thread that handles the connection. The thread is responsible * for opening the connection and for receiving messages. This class contains * several methods that are called by the main class, and that are therefor * executed in a different thread. Note that by using a thread to open the * connection, any blocking of the graphical user interface is avoided. By * using a thread for reading messages sent from the other side, the messages * can be received and posted to the transcript asynchronously at the same * time as the user is typing and sending messages. */ private class ConnectionHandler extends Thread { private volatile ConnectionState state; private String remoteHost; private int port; private Socket socket; private PrintWriter out; private BufferedReader in; private String secret; private String myName; /** * Start a thread for communicating over a socket that is already connected. * The connection comes from the BuddyChat program, but no information has yet been * exchanged. The constructor just starts a thread, which does the actual work. */ ConnectionHandler(Socket connectedSocket, String secret) { postMessage("ACCEPTING CONNECTION REQUEST..."); state = ConnectionState.CONNECTED; socket = connectedSocket; this.secret = secret; start(); } /** * Open a connection to spedified computer and port. The constructor just * starts a thread, which does the actual work. */ ConnectionHandler(String remoteHost, int port, String myName, String partner, String secret) { postMessage("CONNECTING TO " + partner + " (at " + remoteHost + ", port " + port + ")..."); state = ConnectionState.CONNECTING; this.remoteHost = remoteHost; this.port = port; this.secret = secret; this.myName = myName; start(); } /** * Returns the current state of the connection. */ synchronized ConnectionState getConnectionState() { return state; } /** * Send a message to the other side of the connection, and post the * message to the transcript. This should only be called when the * connection state is ConnectionState.CONNECTED; if it is called at * other times, it is ignored. Note that this is called by the * Swing event-handling thread. */ synchronized void send(String message) { if (state == ConnectionState.CONNECTED) { postMessage("SEND: " + message); out.println(message); out.flush(); if (out.checkError()) { postMessage("\nERROR OCCURRED WHILE TRYING TO SEND DATA."); close(); } } } /** * Close the connection. If the socket is non-null, then the socket * is closed, which will cause its input method to fail with an * error. This in turn will cause the ConnectionHandler thread to * terminate. */ synchronized void close() { state = ConnectionState.CLOSED; try { if (socket != null && !socket.isClosed()) socket.close(); } catch (IOException e) { } } /** * This is called by the run() method when a message is received from * the other side of the connection. The message is posted to the * transcript, but only if the connection state is CONNECTED. (This * is because a message might be received after the user has clicked * the "Disconnect" button; that message should not be seen by the * user.) */ synchronized private void received(String message) { if (state == ConnectionState.CONNECTED) postMessage("RECV: " + message); } /** * This is called by the run() method when the connection has been * successfully opened. It enables the correct buttons, writes a * message to the transcript, and sets the connected state to CONNECTED. */ synchronized private void connectionOpened() throws IOException { postMessage("CONNECTION ESTABLISHED.\n"); state = ConnectionState.CONNECTED; sendButton.setEnabled(true); messageInput.setEditable(true); messageInput.setText(""); messageInput.requestFocus(); } /** * This is called by the run() method when the connection is closed * from the other side. (This is detected when an end-of-stream is * encountered on the input stream.) It posts a mesaage to the * transcript and sets the connection state to CLOSED. After calling * this method, the ConnectionHandler thread terminates. */ synchronized private void connectionClosedFromOtherSide() { if (state == ConnectionState.CONNECTED) { postMessage("\nCONNECTION CLOSED FROM OTHER SIDE\n"); state = ConnectionState.CLOSED; } } /** * Clean up after connection is closed. Called from the finally * clause in the run() method. */ synchronized private void cleanup() { state = ConnectionState.CLOSED; sendButton.setEnabled(false); messageInput.setEditable(false); postMessage("\n*** CONNECTION CLOSED ***"); if (socket != null && !socket.isClosed()) { // Make sure that the socket, if any, is closed. try { socket.close(); } catch (IOException e) { } } socket = null; in = null; out = null; } /** * The run() method that is executed by the ConnectionHandler thread. The thread * opens the connection and then reads messages from the other side and posts it * to the transcript until the connection is closed or an error occurs. (Note * that outgoing messages are sent by the event-handling thread, in response to * user actions.) */ public void run() { try { if (state == ConnectionState.CONNECTED) { // The socket was provided by the BuddyChat server and is already // connected. It represents an incoming connection request. // Go through a "handshake" to identify and validate the remote user. InetAddress addr = socket.getInetAddress(); int port = socket.getPort(); postMessage(" (from IP address " + addr + ", port " + port +")"); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream()); String secret = in.readLine(); if (secret == null || !secret.equals(this.secret)) throw new Exception("Connection request does not come from a validated user!"); String partner = in.readLine(); if (partner == null) throw new Exception("Connection unexpectedly closed from other side."); postMessage("Connection opened to " + partner); setTitle("Chatting with " + partner); } else if (state == ConnectionState.CONNECTING) { // The user has requested a request to a remote user. Open a connection // to the user and send handshake info. socket = new Socket(remoteHost,port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream()); out.println(secret); out.println(myName); out.flush(); } connectionOpened(); // Set up to use the connection. while (state == ConnectionState.CONNECTED) { // Read one line of text from the other side of // the connection, and report it to the user. String input = in.readLine(); if (input == null) connectionClosedFromOtherSide(); else received(input); // Report message to user. } } catch (Exception e) { // An error occurred. Report it to the user, but not // if the connection has been closed (since the error // might be the expected error that is generated when // a socket is closed). if (state != ConnectionState.CLOSED) postMessage("\n\n ERROR: " + e); } finally { // Clean up before terminating the thread. cleanup(); } } } // end nested class ConnectionHandler }