import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.io.*; import java.net.*; /** * Opens a window that can be used for a two-way network chat. * The window can "listen" for a connection request on a port * that is specified by the user. It can request a connection * to another GUIChat window on a specified computer and port. * The window has an input box where the user can enter * messages to be sent over the connection. A connection * can be closed by clicking a button in the window or by * closing the window. It is possible to open additional * windows to support simultaneous chats (or to test the program * by opening a connection from one window to another). * This class contains a main() routine, so it can be run as * a stand-alone application. */ public class GUIChat extends JFrame { /** * Possible states of the thread that handles the network connection. */ private enum ConnectionState { LISTENING, CONNECTING, CONNECTED, CLOSED } /** * Default port number. This is the initial content of input boxes in * the window that specify the port number for the connection. */ private static String defaultPort = "1501"; /** * Default host name. This is the initial content of the input box that * specifies the name of the computer to which a connection request * will be sent. */ private static String defaultHost = "localhost"; /** * 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 number of windows that are currently open. If this drops to * zero, then the program is terminated by calling System.exit(); */ private static int openWindowCount; /** * The number of windows that have been created. This is used * in the title bar of the second and subsequent windows. */ private static int windowsCreated; /** * The thread that handles the connection; defined by a nested class. */ private ConnectionHandler connection; /** * The main() routine makes it possible to run this class as an * application; it just creates a GUIChat window and makes it visible. */ public static void main(String[] args) { GUIChat window = new GUIChat(); window.setVisible(true); } /** * Control buttons that appear in the window. */ private JButton newButton, listenButton, connectButton, closeButton, clearButton, quitButton, saveButton, sendButton; /** * Input boxes for connection information (port numbers and host names). */ private JTextField listeningPortInput, remotePortInput, remoteHostInput; /** * 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; /** * Constructor creates a window with a default title. The * constructor does not make the window visible. */ public GUIChat() { this( windowsCreated == 0 ? "Chat Window" : "Chat Window #" + (windowsCreated+1) ); } /** * Constructor creates a window with a specified title. The * constructor does not make the window visible. */ public GUIChat(String title) { super(title); ActionListener actionHandler = new ActionHandler(); newButton = new JButton("New"); newButton.addActionListener(actionHandler); listenButton = new JButton("Listen on port:"); listenButton.addActionListener(actionHandler); connectButton = new JButton("Connect to:"); connectButton.addActionListener(actionHandler); closeButton = new JButton("Disconnect"); closeButton.addActionListener(actionHandler); closeButton.setEnabled(false); clearButton = new JButton("Clear Transcript"); clearButton.addActionListener(actionHandler); sendButton = new JButton("Send"); sendButton.addActionListener(actionHandler); sendButton.setEnabled(false); saveButton = new JButton("Save Transcript"); saveButton.addActionListener(actionHandler); quitButton = new JButton("Quit"); quitButton.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); listeningPortInput = new JTextField(defaultPort,5); remotePortInput = new JTextField(defaultPort,5); remoteHostInput = new JTextField(defaultHost,18); JPanel content = new JPanel(); content.setLayout(new BorderLayout(3,3)); content.setBackground(Color.GRAY); JPanel topPanel = new JPanel(); topPanel.setLayout(new GridLayout(2,1,3,3)); topPanel.setBackground(Color.GRAY); JPanel buttonBar = new JPanel(); buttonBar.setLayout(new FlowLayout(FlowLayout.CENTER,3,3)); JPanel connectBar = new JPanel(); connectBar.setLayout(new FlowLayout(FlowLayout.CENTER,3,3)); JPanel inputBar = new JPanel(); inputBar.setLayout(new BorderLayout(3,3)); inputBar.setBackground(Color.GRAY); content.setBorder(BorderFactory.createLineBorder(Color.GRAY, 3)); content.add(topPanel, BorderLayout.NORTH); topPanel.add(connectBar); topPanel.add(buttonBar); content.add(inputBar, BorderLayout.SOUTH); content.add(new JScrollPane(transcript)); buttonBar.add(newButton); buttonBar.add(quitButton); buttonBar.add(saveButton); buttonBar.add(clearButton); buttonBar.add(closeButton); connectBar.add(listenButton); connectBar.add(listeningPortInput); connectBar.add(Box.createHorizontalStrut(12)); connectBar.add(connectButton); connectBar.add(remoteHostInput); connectBar.add(new JLabel("port:")); connectBar.add(remotePortInput); inputBar.add(new JLabel("Your Message:"), BorderLayout.WEST); inputBar.add(messageInput, BorderLayout.CENTER); inputBar.add(sendButton, BorderLayout.EAST); setContentPane(content); pack(); if (previousWindowLocation == null) // I've added some randomness as a kludge so that if a user // starts two programs on the same machines, both windows will // not be in exactly the same place. This is to make sure the // user can see that there are two windows. previousWindowLocation = new Point((int)(40+30*Math.random()), (int)(80+50*Math.random())); 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); openWindowCount++; windowsCreated++; addWindowListener( new WindowAdapter() { public void windowClosed(WindowEvent evt) { if (connection != null && connection.getConnectionState() != ConnectionState.CLOSED) { connection.close(); } openWindowCount--; if (openWindowCount == 0) { try { System.exit(0); } catch (SecurityException e) { } } } }); } // end constructor /** * Defines responses 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 == newButton) { GUIChat window = new GUIChat(); window.setVisible(true); } else if (source == listenButton) { if (connection == null || connection.getConnectionState() == ConnectionState.CLOSED) { String portString = listeningPortInput.getText(); int port; try { port = Integer.parseInt(portString); if (port < 0 || port > 65535) throw new NumberFormatException(); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(GUIChat.this, portString + "is not a legal port number."); return; } connectButton.setEnabled(false); listenButton.setEnabled(false); closeButton.setEnabled(true); connection = new ConnectionHandler(port); } } else if (source == connectButton) { if (connection == null || connection.getConnectionState() == ConnectionState.CLOSED) { String portString = remotePortInput.getText(); int port; try { port = Integer.parseInt(portString); if (port < 0 || port > 65535) throw new NumberFormatException(); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(GUIChat.this, portString +"is not a legal port number."); return; } connectButton.setEnabled(false); listenButton.setEnabled(false); connection = new ConnectionHandler(remoteHostInput.getText(),port); } } else if (source == closeButton) { if (connection != null) connection.close(); } else if (source == clearButton) { transcript.setText(""); } else if (source == quitButton) { try { System.exit(0); } catch (SecurityException e) { } } 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.YES_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 therefore * 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 ServerSocket listener; private Socket socket; private PrintWriter out; private BufferedReader in; /** * Listen for a connection on a specified port. The constructor * does not perform any network operations; it just sets some * instance variables and starts the thread. Note that the * thread will only listen for one connection, and then will * close its server socket. */ ConnectionHandler(int port) { state = ConnectionState.LISTENING; this.port = port; postMessage("\nLISTENING ON PORT " + port + "\n"); start(); } /** * Open a connection to specified computer and port. The constructor * does not perform any network operations; it just sets some * instance variables and starts the thread. */ ConnectionHandler(String remoteHost, int port) { state = ConnectionState.CONNECTING; this.remoteHost = remoteHost; this.port = port; postMessage("\nCONNECTING TO " + remoteHost + " ON PORT " + port + "\n"); 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. */ 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 server socket is non-null, the * server socket is closed, which will cause its accept() method to * fail with an error. If the socket is non-null, then the socket * is closed, which will cause its input method to fail with an * error. (However, these errors will not be reported to the user.) */ synchronized void close() { state = ConnectionState.CLOSED; try { if (socket != null) socket.close(); else if (listener != null) listener.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("RECEIVE: " + 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 { listener = null; in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream()); state = ConnectionState.CONNECTED; closeButton.setEnabled(true); sendButton.setEnabled(true); messageInput.setEditable(true); messageInput.setText(""); messageInput.requestFocus(); postMessage("CONNECTION ESTABLISHED\n"); } /** * 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 message to the * transcript and sets the connection state to CLOSED. */ synchronized private void connectionClosedFromOtherSide() { if (state == ConnectionState.CONNECTED) { postMessage("\nCONNECTION CLOSED FROM OTHER SIDE\n"); state = ConnectionState.CLOSED; } } /** * Called from the finally clause of the run() method to clean up * after the network connection closes for any reason. */ private void cleanUp() { state = ConnectionState.CLOSED; listenButton.setEnabled(true); connectButton.setEnabled(true); closeButton.setEnabled(false); sendButton.setEnabled(false); messageInput.setEditable(false); postMessage("\n*** CONNECTION CLOSED ***\n"); 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; listener = null; } /** * The run() method that is executed by the thread. It opens a * connection as a client or as a server (depending on which * constructor was used). */ public void run() { try { if (state == ConnectionState.LISTENING) { // Open a connection as a server. listener = new ServerSocket(port); socket = listener.accept(); listener.close(); } else if (state == ConnectionState.CONNECTING) { // Open a connection as a client. socket = new Socket(remoteHost,port); } 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 }