import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.stage.FileChooser; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.control.TextArea; import javafx.scene.control.Alert; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.layout.BorderPane; import javafx.event.ActionEvent; import javafx.geometry.Pos; import javafx.geometry.Insets; 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. To test the program, several * copies of the program can be run on the same computer. */ public class GUIChat extends Application { public static void main(String[] args) { launch(args); } //-------------------------------------------------------------- /** * 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"; /** * The thread that handles the connection; defined by a nested class. */ private volatile ConnectionHandler connection; /** * Control buttons that appear in the window. */ private Button listenButton, connectButton, closeButton, clearButton, quitButton, saveButton, sendButton; /** * Input boxes for connection information (port numbers and host names). */ private TextField listeningPortInput, remotePortInput, remoteHostInput; /** * Input box for messages that will be sent to the other side of the * network connection. */ private TextField messageInput; /** * Contains a transcript of messages sent and received, along with * information about the progress and state of the connection. */ private TextArea transcript; /** * The program's window. */ private Stage window; /** * Set up the GUI and event handling. */ public void start(Stage stage) { window = stage; listenButton = new Button("Listen on port:"); listenButton.setOnAction( this::doAction ); connectButton = new Button("Connect to:"); connectButton.setOnAction( this::doAction ); closeButton = new Button("Disconnect"); closeButton.setOnAction( this::doAction ); closeButton.setDisable(true); clearButton = new Button("Clear Transcript"); clearButton.setOnAction( this::doAction ); sendButton = new Button("Send"); sendButton.setOnAction( this::doAction ); sendButton.setDisable(true); sendButton.setDefaultButton(true); saveButton = new Button("Save Transcript"); saveButton.setOnAction( this::doAction ); quitButton = new Button("Quit"); quitButton.setOnAction( this::doAction ); messageInput = new TextField(); messageInput.setOnAction( this::doAction ); messageInput.setEditable(false); transcript = new TextArea(); transcript.setPrefRowCount(20); transcript.setPrefColumnCount(60); transcript.setWrapText(true); transcript.setEditable(false); listeningPortInput = new TextField(defaultPort); listeningPortInput.setPrefColumnCount(5); remotePortInput = new TextField(defaultPort); remotePortInput.setPrefColumnCount(5); remoteHostInput = new TextField(defaultHost); remoteHostInput.setPrefColumnCount(18); HBox buttonBar = new HBox(5, quitButton, saveButton, clearButton, closeButton); buttonBar.setAlignment(Pos.CENTER); HBox connectBar = new HBox(5, listenButton, listeningPortInput, connectButton, remoteHostInput, new Label("port:"), remotePortInput); connectBar.setAlignment(Pos.CENTER); VBox topPane = new VBox(8, connectBar, buttonBar); BorderPane inputBar = new BorderPane(messageInput); inputBar.setLeft( new Label("Your Message:")); inputBar.setRight(sendButton); BorderPane.setMargin(messageInput, new Insets(0,5,0,5)); BorderPane root = new BorderPane(transcript); root.setTop(topPane); root.setBottom(inputBar); root.setStyle("-fx-border-color: #444; -fx-border-width: 3px"); inputBar.setStyle("-fx-padding:5px; -fx-border-color: #444; -fx-border-width: 3px 0 0 0"); topPane.setStyle("-fx-padding:5px; -fx-border-color: #444; -fx-border-width: 0 0 3px 0"); Scene scene = new Scene(root); stage.setScene(scene); stage.setTitle("Two-user Networked Chat"); stage.setOnHidden( e -> { // If a connection exists when the window is closed, close the connection. if (connection != null) connection.close(); }); stage.show(); } // end start() /** * A little wrapper for showing an error alert. */ private void errorMessage(String message) { Alert alert = new Alert(Alert.AlertType.ERROR, message); alert.showAndWait(); } /** * Defines responses to buttons. (In this program, I use one * method to handle all the buttons; the source of the event * can be used to determine which button was clicked.) */ private void doAction(ActionEvent evt) { Object source = evt.getSource(); 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) { errorMessage(portString + "is not a legal port number."); return; } connectButton.setDisable(true); listenButton.setDisable(true); closeButton.setDisable(false); 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) { errorMessage(portString +"is not a legal port number."); return; } connectButton.setDisable(true); listenButton.setDisable(true); 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 { window.hide(); } 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() { FileChooser fileDialog = new FileChooser(); fileDialog.setInitialFileName("transcript.txt"); fileDialog.setInitialDirectory(new File(System.getProperty("user.home"))); fileDialog.setTitle("Select File to be Saved"); File selectedFile = fileDialog.showSaveDialog(window); if (selectedFile == null) return; // User canceled or clicked the dialog's close box. PrintWriter out; try { FileWriter stream = new FileWriter(selectedFile); out = new PrintWriter( stream ); } catch (Exception e) { errorMessage("Sorry, but an error occurred while\ntrying 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) { errorMessage("Sorry, but an error occurred while\ntrying 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) { Platform.runLater( () -> transcript.appendText(message + '\n') ); } /** * 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. All changes to the GUI * that are made by this class are done using Platform.runLater(). */ 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"); try { setDaemon(true); } catch (Exception e) {} 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"); try { setDaemon(true); } catch (Exception e) {} 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. (Although it is unlikely, it is * possible for this method to block, if the system's buffer for * outgoing data fills.) */ 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; Platform.runLater( () -> { closeButton.setDisable(false); sendButton.setDisable(false); 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; Platform.runLater( () -> { listenButton.setDisable(false); connectButton.setDisable(false); closeButton.setDisable(true); sendButton.setDisable(true); 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 }