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
}