Compare commits

..

No commits in common. "main" and "Class_Message" have entirely different histories.

18 changed files with 514 additions and 1007 deletions

Binary file not shown.

View File

@ -1,79 +1,2 @@
# Übung Multichat # Uebung-hk1-Schrom01-Fassband-Brandleo
Uebung-hk1-Schrom01-Fassband-Brandleo Übung Multichat
# Most important Problems
## Our 5 most important functional Errors
### 1. Multiple Server Connections not possible (Issue [#4](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/4))
If a client was connected to the server, it was not possible to connect another client. The Server created a new instance of ServerConnectionHandler which was in a endless loop to receive data.
<br>
Now we changed the ServerConnectionHandler to implement Interface runnable. If a new client connects to the Server a new Instance of ServerConnectionHandler is created and Method start is called in a new Thread. This makes it possible to connect multiple Clients to the server. The new Thread is alive as long as the client is connected.
### 2. Private Message not visible for Sender (Issue [#28](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/28))
If a Client had sent a Message it was only sent to the server but not displayed directly in the chat window. So only if the message was sent to all clients or if the sender was also the receiver, the message was visible for the sender.
<br>
We discussed two different solutions:
<br>
- Send Messages also to the sender if it's not sent to all clients and if the sender is not the receiver.
- Client checks if a private message is sent and when yes the client adds the message to it's message List.
Both solutions would have the same result. The message would be in the Message List of the sender.
We decided to realise the first solution, because in case of saving the message Lists on server side (as possible extension to restore messages of a user after reconnecting) the messages which are sent by a user have to be added also to the message list of the sender. So it keeps easy to implement this extension by adding those messages to the list, which are sent to a client.
### 3. Empty Messages can be sent(Issue [#7](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/7))
It was possible for a Client to send empty messages and they were also delivered and displayed.
<br>
We discussed two different solutions:
<br>
- If method message in chat window controller is called, only call method message of connection handler if message field is not empty.
- If method message in connection handler is called check if message is empty and return false as boolean like it's already implemented if format isn't correct.
- Do not forward empty messages on server site.
We decided to realise the second solution, because if a Client could have multiple different windows (as possible extension) it would be necessary to implement this check in each window controller with the first solution. With the third solution the server has to process invalid messages and uses resources for that. Also if a user only hets the enter button by mistake. With the second solution the connection handler returns false as feedback if the format of the message was invalid and the user sees that he did a unwanted action.
### 4. Direct Messages not possible when username has special characters (Issues [#19](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/19), [#29](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/29))
If a user wants to send a private message to a user with special characters or spaces in the username, it's not delivered because the user is not found.
We were able to solve this problem by changing the regex of the "messagePattern". Instead of using "\w" as characters (contains only letters, numbers and underscores) for the username we used "\S" (contains all characters excepting spaces). During the registration process we check if a username contains any spaces don't accept it in this case. Accepting spaces in a Username would not be possible becuase it's used to separate the username and the message when a user types a private message.
### 5. Message stays in input field after being sent (Issue [#8](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/8))
If a User had sent a message, the Message wasn't removed from the message field.
We decided to remove the message only if the message was sent sucessfully. So if there is a typing mistake in the username of the receiver, the user can do a correction without having to rewrite the whole message.
The message field is cleared if the method message of the connection handler returns true.
## Our 5 most important structural problems
### 1. Data Class Message (Issues [#14](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/14), [#49](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/49))
The class MessageList had multiple List objects to save the different informations about a message. We created a Message Class which will be stored in a Array List in the MessageList instance and can also be sent from the server to a client and from a client to the server.
<br>
The new class message contains the datafields type, sender, receiver, text and also the enum with the different data types. We also added a method to check if a message matches a filter, which can be used in the method getFilteredMessages in MessageList.
### 2. ClientConnectionHandler and ServerConnectionHandler have a lot in common (Issue [#11](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/11))
We created a superclass Connectionhandler to remove code dupplication. Common datafields and method "sendData" are now in superclass and can be used in ServerConnectionHandler and ClientConnectionHandler.
### 3. Creation of instances of ClientMessageList and ClientConnectionHandler (Issues [#18](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/18)[#24](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/24)), [#24](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/24))
The instances of ClientMessageList and ClientConnectionHandler had been created and stored only in ChatwindowController. Those Classes are acting as model in our application. The should not be created in a window controller. So we created methods to set those objects in chat window controller and call those methods from clientUI Class.
### 4. ClientConnectionHandler calls Methods on ChatWindowController to add new Messages (Issue [#23](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/23))
A Object of ChatWindowController was saved in ClientConnectionHandler and methods of window controller were called to add messages in the window. A Model should not call any methods of a window controller and should work without any controller object. We removed those method calls and added listeners to those properties in ChatWindowController to update the view if there are any changes in the models.
### 5. ChatWindowController MessagePattern and method message (Issue [#17](https://github.zhaw.ch/PM2-IT21bWIN-ruiz-mach-krea/Uebung-hk1-Schrom01-Fassband-Brandleo/issues/17))
The Method "message" in ChatWindowController had a lot of logic in it and a MessagePattern object was saved in this class for that. We moved the logic to get the receiver and the message text into class ConnectionHandler, because the ConnectionHandler has to send the message to the server and it returns a boolean to give feedback if it was successful.
# Description of our solution
### Classdiagram of our new structure
![Classdiagram of this program](./classdiagramm.svg)
### Structure of Client:
The instances of ClientConnectionHandler and ClientMessageList are not created in the ChatWindowControllerClass anymore. It's created in the ClientUI Class and the ChatWindowController Class has access to them. This makes it possible for a client to have multiple windows open or close all windows without losing the message list or closing the connection.
The ClientConnectionHandlder does't need a ChatWindowController Object now. Received Messages are added directly into the message list. This makes it possible to receive and store new messages without having a Window open.
The ChatWindowController adds listeners in the clientConnectionHandler and in ClientMessageList to refresh the view if there are new Messages or if there are any changes in the connection handler.
### Structure of protocol:
#### Datatype:
A new Class Message represents a Message which can be sent from a Client to the Server and from the Server to a Client and can also directly be stored in ClientMessageList. This makes it much easier to process the received data and to send a message because no String parsing and String building is needed.
#### Class ConnectionHandler
A new Abstract Class ConnectionHandler contains Code which is used in ClientConnectionHandler and ServerConnectionHandler. For Example Method send Data. This removes a lot of duplicated methods and data fields.
### Ideas to extend the application:
- It would be easyer to use for a User if it was possible to open a separate Window for a private Chat with a specific user.
- Users could be divided into groups, and it would be nice if a group name could be used as receiver.
- registration and login with username and password and store credentials in a Database in Server.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 135 KiB

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-04-08T11:09:07.435Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/16.5.1 Chrome/96.0.4664.110 Electron/16.0.7 Safari/537.36" etag="9tAKDfXeZvsv2s69LJ1W" version="16.5.1" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram></mxfile>

View File

@ -1,12 +1,9 @@
package ch.zhaw.pm2.multichat.client; package ch.zhaw.pm2.multichat.client;
import ch.zhaw.pm2.multichat.protocol.ConnectionHandler; import ch.zhaw.pm2.multichat.client.ClientConnectionHandler.State;
import ch.zhaw.pm2.multichat.protocol.ConnectionHandler.State;
import ch.zhaw.pm2.multichat.protocol.ChatProtocolException; import ch.zhaw.pm2.multichat.protocol.ChatProtocolException;
import ch.zhaw.pm2.multichat.protocol.Message; import ch.zhaw.pm2.multichat.protocol.NetworkHandler;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -16,97 +13,63 @@ import javafx.scene.layout.Pane;
import javafx.stage.WindowEvent; import javafx.stage.WindowEvent;
import java.io.IOException; import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ch.zhaw.pm2.multichat.protocol.ConnectionHandler.State.*; import static ch.zhaw.pm2.multichat.client.ClientConnectionHandler.State.*;
/**
* Class Representing the Controller Element of the Window, also Contains the Elements that contacts the View Elements via
* Listeners and Observable Objects.
* To Contact the Model Elements needed it also holds references to message and the Connectionhandler.
*/
public class ChatWindowController { public class ChatWindowController {
private ClientConnectionHandler connectionHandler; private ClientConnectionHandler connectionHandler;
private ClientMessageList messages; private ClientMessageList messages;
private final WindowCloseHandler windowCloseHandler = new WindowCloseHandler(); private final WindowCloseHandler windowCloseHandler = new WindowCloseHandler();
@FXML @FXML private Pane rootPane;
private Pane rootPane; @FXML private TextField serverAddressField;
@FXML @FXML private TextField serverPortField;
private TextField serverAddressField; @FXML private TextField userNameField;
@FXML @FXML private TextField messageField;
private TextField serverPortField; @FXML private TextArea messageArea;
@FXML @FXML private Button connectButton;
private TextField userNameField; @FXML private Button sendButton;
@FXML @FXML private TextField filterValue;
private TextField messageField;
@FXML
private TextArea messageArea; @FXML
@FXML public void initialize() {
private Button connectButton; serverAddressField.setText(NetworkHandler.DEFAULT_ADDRESS.getCanonicalHostName());
@FXML serverPortField.setText(String.valueOf(NetworkHandler.DEFAULT_PORT));
private TextField filterValue; stateChanged(NEW);
@FXML }
private Button sendButton;
/**
* Takes a message object and stores it used as Model and also starts message Listener via messageListener method.
*
* @param messages Object that will be set for use as Model
*/
public void setMessages(ClientMessageList messages) { public void setMessages(ClientMessageList messages) {
this.messages = messages; this.messages = messages;
messageListener();
} }
/**
* Takes a Connection handler object and stores it used as Model and also starts a Listener for it.
*
* @param connectionHandler that will be set and used as Model.
*/
public void setConnectionHandler(ClientConnectionHandler connectionHandler) {
this.connectionHandler = connectionHandler;
startConnectionHandlerListener();
serverAddressField.setText(connectionHandler.getServerAddressProperty().get());
serverPortField.setText(String.valueOf(connectionHandler.getServerPortProperty().get()));
refreshConnectionState(connectionHandler.getStateProperty().get());
}
/**
* Method which closes the Application via use of the disconnect Method.
*/
private void applicationClose() { private void applicationClose() {
disconnect(); connectionHandler.setState(DISCONNECTED);
} }
/**
* Method that handles the Connect Button and Initiates connect when connectionHandler is not Connected or
* a disconnect when it is connected.
*/
@FXML @FXML
private void toggleConnection() { private void toggleConnection () {
if (connectionHandler == null || connectionHandler.getStateProperty().get() != CONNECTED) { if (connectionHandler == null || connectionHandler.getState() != CONNECTED) {
connect(); connect();
} else { } else {
disconnect(); disconnect();
} }
} }
/**
* Initiates a connection by starting Connection Handler and telling, the Handler to start a connection.
*/
private void connect() { private void connect() {
try { try {
messages.clear(); // clear message list messages.clear(); // clear message list
redrawMessageList();
startConnectionHandler(); startConnectionHandler();
connectionHandler.connect(); connectionHandler.connect();
} catch (ChatProtocolException | IOException e) { } catch(ChatProtocolException | IOException e) {
addError("Error while starting Connection Handler and connect" + e); addError(e.getMessage());
} }
} }
/**
* Initiates disconnecting of the connectionHandler, also checks if connectionHandler is available.
*/
private void disconnect() { private void disconnect() {
if (connectionHandler == null) { if (connectionHandler == null) {
addError("No connection handler"); addError("No connection handler");
@ -119,9 +82,6 @@ public class ChatWindowController {
} }
} }
/**
* Method which is used when the send button is pressed and handing over a message to the Connection Handler
*/
@FXML @FXML
private void message() { private void message() {
String messageString = messageField.getText().strip(); String messageString = messageField.getText().strip();
@ -138,66 +98,46 @@ public class ChatWindowController {
} }
} }
/**
* Method which is used when a Filter is applied
* Setting the Text in the message area after sending it through the filter.
*/
@FXML @FXML
private void applyFilter() { private void applyFilter( ) {
Platform.runLater(() -> this.messageArea.setText(messages.getFilteredMessages(filterValue.getText().strip()))); this.redrawMessageList();
} }
/**
* Starts the ConnectionHandler setting the username and Checking if the name follows the valid format of no spaces.
*
* @throws IOException for error that may occur during initialization of connectionHandler.
*/
private void startConnectionHandler() throws IOException { private void startConnectionHandler() throws IOException {
String userName = userNameField.getText(); String userName = userNameField.getText();
if (!userName.contains(" ")) {
String serverAddress = serverAddressField.getText(); String serverAddress = serverAddressField.getText();
int serverPort = Integer.parseInt(serverPortField.getText()); int serverPort = Integer.parseInt(serverPortField.getText());
connectionHandler.initialize(serverAddress, serverPort, userName); connectionHandler = new ClientConnectionHandler(
NetworkHandler.openConnection(serverAddress, serverPort), userName,
this);
new Thread(connectionHandler).start(); new Thread(connectionHandler).start();
// register window close handler
rootPane.getScene().getWindow().addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler); rootPane.getScene().getWindow().addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler);
} else { }
addError("It is not allowed to have spaces in username!");
private void terminateConnectionHandler() {
// unregister window close handler
rootPane.getScene().getWindow().removeEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler);
if (connectionHandler != null) {
connectionHandler.stopReceiving();
connectionHandler = null;
} }
} }
/** public void stateChanged(State newState) {
* Sets the state shown according to the state the method receives, if state indicates disconnected it will also inform the
* Connection Handler and tell it to stop Receiving more messages.
*
* @param newState is the state that it should be set to.
*/
public void refreshConnectionState(State newState) {
// update UI (need to be run in UI thread: see Platform.runLater()) // update UI (need to be run in UI thread: see Platform.runLater())
Platform.runLater(new Runnable() { Platform.runLater(new Runnable() {
@Override @Override
public void run() { public void run() {
if(newState == CONNECTED || newState == CONFIRM_DISCONNECT){ connectButton.setText((newState == CONNECTED || newState == CONFIRM_DISCONNECT) ? "Disconnect" : "Connect");
connectButton.setText("Disconnect");
messageField.setDisable(false);
sendButton.setDisable(false);
} else {
connectButton.setText("Connect");
messageField.setDisable(true);
sendButton.setDisable(true);
}
} }
}); });
if (newState == DISCONNECTED) { if (newState == DISCONNECTED) {
connectionHandler.stopReceiving(); terminateConnectionHandler();
} }
} }
/**
* Sets displayed username according to the String provided.
*
* @param userName provided String that is set as name.
*/
public void setUserName(String userName) { public void setUserName(String userName) {
Platform.runLater(new Runnable() { Platform.runLater(new Runnable() {
@Override @Override
@ -207,11 +147,6 @@ public class ChatWindowController {
}); });
} }
/**
* Sets displayed Server Address.
*
* @param serverAddress provided String that is set as server address.
*/
public void setServerAddress(String serverAddress) { public void setServerAddress(String serverAddress) {
Platform.runLater(new Runnable() { Platform.runLater(new Runnable() {
@Override @Override
@ -221,11 +156,6 @@ public class ChatWindowController {
}); });
} }
/**
* Sets displayed Server port.
*
* @param serverPort provided String that is set as server port.
*/
public void setServerPort(int serverPort) { public void setServerPort(int serverPort) {
Platform.runLater(new Runnable() { Platform.runLater(new Runnable() {
@Override @Override
@ -235,71 +165,31 @@ public class ChatWindowController {
}); });
} }
/** public void addMessage(String sender, String receiver, String message) {
* Method which adds an incoming String as an Error Message. messages.addMessage(new Message(Message.MessageType.MESSAGE, sender, receiver, message));
* this.redrawMessageList();
* @param message String to be added as Error
*/
public void addError(String message) {
messages.addMessage(new Message(ConnectionHandler.DATA_TYPE.DATA_TYPE_ERROR, null, null, message));
} }
/** public void addInfo(String message) {
* Nested Class in charge of Closing the window messages.addMessage(new Message(Message.MessageType.INFO, null, null, message));
*/ this.redrawMessageList();
class WindowCloseHandler implements EventHandler<WindowEvent> { }
/** public void addError(String message) {
* @param event the event which occurred when Windows is closed messages.addMessage(new Message(Message.MessageType.ERROR, null, null, message));
*/ this.redrawMessageList();
}
private void redrawMessageList() {
this.messageArea.clear();
Platform.runLater(() -> this.messageArea.setText(messages.getFilteredMessages(filterValue.getText().strip())));
}
class WindowCloseHandler implements EventHandler<WindowEvent> {
public void handle(WindowEvent event) { public void handle(WindowEvent event) {
applicationClose(); applicationClose();
} }
}
/**
* Starts several new Listener for Connection Handler changes by using several observable properties.
*/
public void startConnectionHandlerListener() {
connectionHandler.getStateProperty().addListener(new ChangeListener<State>() {
@Override
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
refreshConnectionState(newValue);
}
});
connectionHandler.getUserNameProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
setUserName(newValue);
}
});
connectionHandler.getServerAddressProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
setServerAddress(newValue);
}
});
connectionHandler.getServerPortProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
setServerPort(newValue.intValue());
}
});
}
/**
* Starts a new Listener for messages by using the observable Boolean.
*/
private void messageListener() {
messages.getChangedProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
Platform.runLater(() -> messageArea.setText(messages.getFilteredMessages(filterValue.getText().strip())));
}
});
} }
} }

View File

@ -2,16 +2,8 @@ package ch.zhaw.pm2.multichat.client;
import javafx.application.Application; import javafx.application.Application;
/**
* Client Main Class in charge of starting the UI only contains main method.
*/
public class Client { public class Client {
/**
* Main method which launches the Client UI
*
* @param args no arguments needed
*/
public static void main(String[] args) { public static void main(String[] args) {
// Start UI // Start UI
System.out.println("Starting Client Application"); System.out.println("Starting Client Application");

View File

@ -1,12 +1,7 @@
package ch.zhaw.pm2.multichat.client; package ch.zhaw.pm2.multichat.client;
import ch.zhaw.pm2.multichat.protocol.ChatProtocolException; import ch.zhaw.pm2.multichat.protocol.ChatProtocolException;
import ch.zhaw.pm2.multichat.protocol.ConnectionHandler;
import ch.zhaw.pm2.multichat.protocol.Message;
import ch.zhaw.pm2.multichat.protocol.NetworkHandler; import ch.zhaw.pm2.multichat.protocol.NetworkHandler;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
@ -15,118 +10,61 @@ import java.util.Scanner;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static ch.zhaw.pm2.multichat.protocol.ConnectionHandler.State.*; import static ch.zhaw.pm2.multichat.client.ClientConnectionHandler.State.*;
/** public class ClientConnectionHandler implements Runnable {
* Client Connection Handler Class is used for the connection of the Client to a server private final NetworkHandler.NetworkConnection<String> connection;
* it is used part as a Model storing properties like userName, state, serverAddress and Port. private final ChatWindowController controller;
* Also holds methods to
*/
public class ClientConnectionHandler extends ConnectionHandler implements Runnable {
private final Pattern messagePattern = Pattern.compile("^(?:@(\\S*))?\\s*(.*)$"); // Data types used for the Chat Protocol
private static final String DATA_TYPE_CONNECT = "CONNECT";
private static final String DATA_TYPE_CONFIRM = "CONFIRM";
private static final String DATA_TYPE_DISCONNECT = "DISCONNECT";
private static final String DATA_TYPE_MESSAGE = "MESSAGE";
private static final String DATA_TYPE_ERROR = "ERROR";
private final SimpleStringProperty userName; public static final String USER_NONE = "";
private final SimpleObjectProperty<State> state; public static final String USER_ALL = "*";
private final ClientMessageList messages;
private final SimpleStringProperty serverAddress;
private final SimpleIntegerProperty serverPort;
/** private final Pattern messagePattern = Pattern.compile( "^(?:@(\\w*))?\\s*(.*)$" );
* Constructor initializes ConnectionHandler by Setting default values for the fields and stores the given messages itself.
* private String userName = USER_NONE;
* @param messages private State state = NEW;
*/
public ClientConnectionHandler(ClientMessageList messages) { enum State {
super(); NEW, CONFIRM_CONNECT, CONNECTED, CONFIRM_DISCONNECT, DISCONNECTED;
this.messages = messages;
state = new SimpleObjectProperty<>(State.NEW);
serverAddress = new SimpleStringProperty(NetworkHandler.DEFAULT_ADDRESS.getCanonicalHostName());
serverPort = new SimpleIntegerProperty(NetworkHandler.DEFAULT_PORT);
this.userName = new SimpleStringProperty(null);
} }
/** public ClientConnectionHandler(NetworkHandler.NetworkConnection<String> connection,
* Called to initialize the ClientConnectionHandler when trying to start a connection String userName,
* ChatWindowController controller) {
* @param serverAddress to connect to this.connection = connection;
* @param serverPort to connect to this.userName = (userName == null || userName.isBlank())? USER_NONE : userName;
* @param userName to connect as this.controller = controller;
* @throws IOException if connection to Server not possible
*/
public void initialize(String serverAddress, int serverPort, String userName) throws IOException {
state.set(NEW);
this.serverAddress.set(serverAddress);
this.serverPort.set(serverPort);
setConnection(NetworkHandler.openConnection(serverAddress, serverPort));
this.userName.set((userName == null || userName.isBlank()) ? USER_NONE : userName);
} }
/** public State getState() {
* Observable getter Method for the stored serverAddress
*
* @return the stored serverAddress
*/
public SimpleStringProperty getServerAddressProperty() {
return serverAddress;
}
/**
* Observable getter Method for the stored serverPort
*
* @return the stored serverPort
*/
public SimpleIntegerProperty getServerPortProperty() {
return serverPort;
}
/**
* Observable getter Method for the stored state
*
* @return the stored state
*/
public SimpleObjectProperty<State> getStateProperty() {
return this.state; return this.state;
} }
/** public void setState (State newState) {
* Observable getter Method for the stored userName this.state = newState;
* controller.stateChanged(newState);
* @return the stored userName
*/
public SimpleStringProperty getUserNameProperty() {
return userName;
} }
/** public void run () {
* Method which sets a new State.
*
* @param newState the state that will be set
*/
public void setState(State newState) {
state.set(newState);
}
/**
* Standard run method which will directly start the startReceiving method.
*/
public void run() {
startReceiving(); startReceiving();
} }
/** public void startReceiving() {
* Method that is started by the run method, starts a connection handler.
* Figures out if connection is available if not determines the error cause and gives an error accordingly.
*/
private void startReceiving() {
System.out.println("Starting Connection Handler"); System.out.println("Starting Connection Handler");
try { try {
System.out.println("Start receiving data..."); System.out.println("Start receiving data...");
while (getConnection().isAvailable()) { while (connection.isAvailable()) {
Message data = getConnection().receive(); String data = connection.receive();
processData(data); processData(data);
} }
System.out.println("Stopped receiving data"); System.out.println("Stopped recieving data");
} catch (SocketException e) { } catch (SocketException e) {
System.out.println("Connection terminated locally"); System.out.println("Connection terminated locally");
this.setState(DISCONNECTED); this.setState(DISCONNECTED);
@ -135,22 +73,19 @@ public class ClientConnectionHandler extends ConnectionHandler implements Runnab
System.out.println("Connection terminated by remote"); System.out.println("Connection terminated by remote");
this.setState(DISCONNECTED); this.setState(DISCONNECTED);
System.err.println("Unregistered because connection terminated" + e.getMessage()); System.err.println("Unregistered because connection terminated" + e.getMessage());
} catch (IOException e) { } catch(IOException e) {
System.err.println("Communication error" + e); System.err.println("Communication error" + e);
} catch (ClassNotFoundException e) { } catch(ClassNotFoundException e) {
System.err.println("Received object of unknown type" + e.getMessage()); System.err.println("Received object of unknown type" + e.getMessage());
} }
System.out.println("Stopped Connection Handler"); System.out.println("Stopped Connection Handler");
} }
/**
* Method which is used to stop receiving data, gets the current connection and closes it.
*/
public void stopReceiving() { public void stopReceiving() {
System.out.println("Closing Connection Handler to Server"); System.out.println("Closing Connection Handler to Server");
try { try {
System.out.println("Stop receiving data..."); System.out.println("Stop receiving data...");
getConnection().close(); connection.close();
System.out.println("Stopped receiving data."); System.out.println("Stopped receiving data.");
} catch (IOException e) { } catch (IOException e) {
System.err.println("Failed to close connection." + e.getMessage()); System.err.println("Failed to close connection." + e.getMessage());
@ -158,129 +93,127 @@ public class ClientConnectionHandler extends ConnectionHandler implements Runnab
System.out.println("Closed Connection Handler to Server"); System.out.println("Closed Connection Handler to Server");
} }
/** private void processData(String data) {
* Method which processes data and determines its type then uses the corresponding method to process it. try {
* // parse data content
* @param data that is received in a form of a String and then used depending on its determined cause. Scanner scanner = new Scanner(data);
*/ String sender = null;
private void processData(Message data) { String reciever = null;
String type = null;
String payload = null;
if (scanner.hasNextLine()) {
sender = scanner.nextLine();
} else {
throw new ChatProtocolException("No Sender found");
}
if (scanner.hasNextLine()) {
reciever = scanner.nextLine();
} else {
throw new ChatProtocolException("No Reciever found");
}
if (scanner.hasNextLine()) {
type = scanner.nextLine();
} else {
throw new ChatProtocolException("No Type found");
}
if (scanner.hasNextLine()) {
payload = scanner.nextLine();
}
// dispatch operation based on type parameter // dispatch operation based on type parameter
if (data.getType() == DATA_TYPE.DATA_TYPE_CONNECT) { if (type.equals(DATA_TYPE_CONNECT)) {
System.err.println("Illegal connect request from server"); System.err.println("Illegal connect request from server");
} else if (data.getType() == DATA_TYPE.DATA_TYPE_CONFIRM) { } else if (type.equals(DATA_TYPE_CONFIRM)) {
caseConfirm(data); if (state == CONFIRM_CONNECT) {
} else if (data.getType() == DATA_TYPE.DATA_TYPE_DISCONNECT) { this.userName = reciever;
caseDisconnect(data); controller.setUserName(userName);
} else if (data.getType() == DATA_TYPE.DATA_TYPE_MESSAGE) { controller.setServerPort(connection.getRemotePort());
caseMessage(data); controller.setServerAddress(connection.getRemoteHost());
} else if (data.getType() == DATA_TYPE.DATA_TYPE_ERROR) { controller.addInfo(payload);
caseError(data); System.out.println("CONFIRM: " + payload);
} else {
System.out.println("Unknown data type received: " + data.getType());
}
}
private void caseConfirm(Message data) {
if (state.get() == CONFIRM_CONNECT) {
this.userName.set(data.getReceiver());
this.serverPort.set(getConnection().getRemotePort());
this.serverAddress.set(getConnection().getRemoteHost());
messages.addMessage(data);
System.out.println("CONFIRM: " + data.getText());
this.setState(CONNECTED); this.setState(CONNECTED);
} else if (state.get() == CONFIRM_DISCONNECT) { } else if (state == CONFIRM_DISCONNECT) {
messages.addMessage(data); controller.addInfo(payload);
System.out.println("CONFIRM: " + data.getText()); System.out.println("CONFIRM: " + payload);
this.setState(DISCONNECTED); this.setState(DISCONNECTED);
} else { } else {
System.err.println("Got unexpected confirm message: " + data.getText()); System.err.println("Got unexpected confirm message: " + payload);
} }
} } else if (type.equals(DATA_TYPE_DISCONNECT)) {
if (state == DISCONNECTED) {
/** System.out.println("DISCONNECT: Already in disconnected: " + payload);
* Initiates the disconnect sequence and sends the message with all its info.
*
* @param data Data which has been transmitted
*/
private void caseDisconnect(Message data) {
if (state.get() == DISCONNECTED) {
System.out.println("DISCONNECT: Already in disconnected: " + data.getText());
return; return;
} }
messages.addMessage(data); controller.addInfo(payload);
System.out.println("DISCONNECT: " + data.getText()); System.out.println("DISCONNECT: " + payload);
this.setState(DISCONNECTED); this.setState(DISCONNECTED);
} } else if (type.equals(DATA_TYPE_MESSAGE)) {
if (state != CONNECTED) {
/** System.out.println("MESSAGE: Illegal state " + state + " for message: " + payload);
* Initiates the procedure to send a new message and sends one as such.
*
* @param data Data which has been transmitted
*/
private void caseMessage(Message data) {
if (state.get() != CONNECTED) {
System.out.println("MESSAGE: Illegal state " + state + " for message: " + data.getText());
return; return;
} }
messages.addMessage(data); controller.addMessage(sender, reciever, payload);
System.out.println("MESSAGE: From " + data.getSender() + " to " + data.getReceiver() + ": " + data.getText()); System.out.println("MESSAGE: From " + sender + " to " + reciever + ": "+ payload);
} else if (type.equals(DATA_TYPE_ERROR)) {
controller.addError(payload);
System.out.println("ERROR: " + payload);
} else {
System.out.println("Unknown data type received: " + type);
}
} catch (ChatProtocolException e) {
System.err.println("Error while processing data: " + e.getMessage());
sendData(USER_NONE, userName, DATA_TYPE_ERROR, e.getMessage());
}
} }
/** public void sendData(String sender, String receiver, String type, String payload) {
* Stores the message as an error message and displays it as such as well. if (connection.isAvailable()) {
* new StringBuilder();
* @param data Data which has been transmitted String data = new StringBuilder()
*/ .append(sender+"\n")
private void caseError(Message data) { .append(receiver+"\n")
messages.addMessage(data); .append(type+"\n")
System.out.println("ERROR: " + data.getText()); .append(payload+"\n")
.toString();
try {
connection.send(data);
} catch (SocketException e) {
System.err.println("Connection closed: " + e.getMessage());
} catch (EOFException e) {
System.out.println("Connection terminated by remote");
} catch(IOException e) {
System.err.println("Communication error: " + e.getMessage());
}
}
} }
/**
* Send connect attempt to Server
*
* @throws ChatProtocolException Error that is thrown if the state is not set to NEW
*/
public void connect() throws ChatProtocolException { public void connect() throws ChatProtocolException {
if (state.get() != NEW) throw new ChatProtocolException("Illegal state for connect: " + state); if (state != NEW) throw new ChatProtocolException("Illegal state for connect: " + state);
this.sendData(userName.get(), USER_NONE, getDataTypeConnect(), null); this.sendData(userName, USER_NONE, DATA_TYPE_CONNECT,null);
this.setState(CONFIRM_CONNECT); this.setState(CONFIRM_CONNECT);
} }
/**
* Send Disconnect attempt to server
*
* @throws ChatProtocolException Error tha si thrown if state is invalid
*/
public void disconnect() throws ChatProtocolException { public void disconnect() throws ChatProtocolException {
if (state.get() != NEW && state.get() != CONNECTED) if (state != NEW && state != CONNECTED) throw new ChatProtocolException("Illegal state for disconnect: " + state);
throw new ChatProtocolException("Illegal state for disconnect: " + state); this.sendData(userName, USER_NONE, DATA_TYPE_DISCONNECT,null);
this.sendData(userName.get(), USER_NONE, getDataTypeDisconnect(), null);
this.setState(CONFIRM_DISCONNECT); this.setState(CONFIRM_DISCONNECT);
} }
/**
* Sends message to Server
*
* @param messageString The message the user wants to send
* @return true if message is valid else false
* @throws ChatProtocolException if illegal connection state
*/
public boolean message(String messageString) throws ChatProtocolException { public boolean message(String messageString) throws ChatProtocolException {
if (state.get() != CONNECTED) throw new ChatProtocolException("Illegal state for message: " + state); if (state != CONNECTED) throw new ChatProtocolException("Illegal state for message: " + state);
Matcher matcher = messagePattern.matcher(messageString); Matcher matcher = messagePattern.matcher(messageString);
if (matcher.find()) { if (matcher.find()) {
String receiver = matcher.group(1); String receiver = matcher.group(1);
String message = matcher.group(2); String message = matcher.group(2);
if (message.length() < 1) { if(message.length() < 1){
return false; return false;
} }
if (receiver == null || receiver.isBlank()) receiver = ClientConnectionHandler.USER_ALL; if (receiver == null || receiver.isBlank()) receiver = ClientConnectionHandler.USER_ALL;
this.sendData(userName.get(), receiver, getDataTypeMessage(), message); this.sendData(userName, receiver, DATA_TYPE_MESSAGE,message);
return true; return true;
} else { } else {
return false; return false;
} }
} }
} }

View File

@ -1,46 +1,26 @@
package ch.zhaw.pm2.multichat.client; package ch.zhaw.pm2.multichat.client;
import ch.zhaw.pm2.multichat.protocol.ConnectionHandler;
import ch.zhaw.pm2.multichat.protocol.Message;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* Class that is used to store the messages in an ArrayList
* And also as a Model that informs the controller via Listeners that can be initialized
*/
public class ClientMessageList { public class ClientMessageList {
private List<Message> messages = new ArrayList<>(); private List<Message> messages = new ArrayList<>();
private final SimpleBooleanProperty changed = new SimpleBooleanProperty(false);
/**
* Adds a new message to ArrayList and also informs Listener.
*
* @param message that should be added
*/
public void addMessage(Message message) { public void addMessage(Message message) {
messages.add(message); messages.add(message);
changed.set(!changed.get());
} }
/**
* Applies a given filter over all messages and returns the result as a new String.
* Method is also in charge of applying the correct prefix to the message according to its type.
*
* @param filter is the applied filter on all the messages that are stored.
* @return String that matches the given filter.
*/
public String getFilteredMessages(String filter) { public String getFilteredMessages(String filter) {
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
boolean showAll = filter == null || filter.isBlank(); boolean showAll = filter == null || filter.isBlank();
for (Message message : messages) { for(Message message : messages) {
if (showAll || message.matchesFilter(filter)) { if(showAll || message.matchesFilter(filter))
{
switch (message.getType()) { switch (message.getType()) {
case DATA_TYPE_MESSAGE -> result.append(String.format("[%s -> %s] %s\n", message.getSender(), message.getReceiver(), message.getText())); case MESSAGE -> result.append(String.format("[%s -> %s] %s\n", message.getSender(), message.getReceiver(), message.getText()));
case DATA_TYPE_ERROR -> result.append(String.format("[ERROR] %s\n", message.getText())); case ERROR -> result.append(String.format("[ERROR] %s\n", message.getText()));
case DATA_TYPE_CONFIRM, DATA_TYPE_DISCONNECT, DATA_TYPE_CONNECT -> result.append(String.format("[INFO] %s\n", message.getText())); case INFO -> result.append(String.format("[INFO] %s\n", message.getText()));
default -> result.append(String.format("[ERROR] %s\n", "Unexpected message type: " + message.getType())); default -> result.append(String.format("[ERROR] %s\n", "Unexpected message type: " + message.getType()));
} }
} }
@ -48,21 +28,8 @@ public class ClientMessageList {
return result.toString(); return result.toString();
} }
/**
* Overwrites the Arraylist of messages, clearing it, also informs all Listeners.
*/
public void clear() { public void clear() {
messages = new ArrayList<>(); messages = new ArrayList<>();
changed.set(!changed.get());
}
/**
* Getter Method to check the current value of SimpleBooleanProperty changed.
*
* @return the current value of changed
*/
public SimpleBooleanProperty getChangedProperty() {
return changed;
} }
} }

View File

@ -6,32 +6,14 @@ import javafx.scene.Scene;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.stage.Stage; import javafx.stage.Stage;
/**
* Class in charge of initializing and loading elements needed for the GUI.
* Also Launching the chat window with pre-given parameters
*/
public class ClientUI extends Application { public class ClientUI extends Application {
private final ClientMessageList clientMessageList = new ClientMessageList(); private ClientMessageList clientMessageList = new ClientMessageList();
private final ClientConnectionHandler connectionHandler = new ClientConnectionHandler(clientMessageList);
/**
* Start method will only run the chatWindow method.
*
* @param primaryStage the primary stage for this application, onto which
* the application scene can be set.
* Applications may create other stages, if needed, but they will not be
* primary stages.
*/
@Override @Override
public void start(Stage primaryStage) { public void start(Stage primaryStage) {
chatWindow(primaryStage); chatWindow(primaryStage);
} }
/**
* Method is in charge of loading the FXML file and then initializing the controllers and setting some parameters for the stage.
*
* @param primaryStage of the Application onto which the Scene is set.
*/
private void chatWindow(Stage primaryStage) { private void chatWindow(Stage primaryStage) {
try { try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("ChatWindow.fxml")); FXMLLoader loader = new FXMLLoader(getClass().getResource("ChatWindow.fxml"));
@ -39,7 +21,6 @@ public class ClientUI extends Application {
ChatWindowController chatWindowController = loader.getController(); ChatWindowController chatWindowController = loader.getController();
chatWindowController.setMessages(clientMessageList); chatWindowController.setMessages(clientMessageList);
chatWindowController.setConnectionHandler(connectionHandler);
// fill in scene and stage setup // fill in scene and stage setup
Scene scene = new Scene(rootPane); Scene scene = new Scene(rootPane);
@ -47,11 +28,11 @@ public class ClientUI extends Application {
// configure and show stage // configure and show stage
primaryStage.setScene(scene); primaryStage.setScene(scene);
primaryStage.setMinWidth(420);
primaryStage.setMinHeight(250); primaryStage.setMinHeight(250);
primaryStage.setTitle("Multichat Client"); primaryStage.setTitle("Multichat Client");
primaryStage.show(); primaryStage.show();
primaryStage.setMinWidth(primaryStage.getWidth()); //use automatically computed size as Minimum Size. } catch(Exception e) {
} catch (Exception e) {
System.err.println("Error starting up UI" + e.getMessage()); System.err.println("Error starting up UI" + e.getMessage());
} }
} }

View File

@ -0,0 +1,71 @@
package ch.zhaw.pm2.multichat.client;
/**
A Message object represents one Message of a client. Can be stored in ClientMessageList.
*/
public class Message {
private MessageType type;
private String sender;
private String receiver;
private String text;
/**
* Constructor of Message. Needs all Information about a Message to save them.
* @param type Message (if it's a message typed by a user), Error or Information (if it is generated automatically, in this case sender and reciever will be null)
* @param sender The User who has sent the message.
* @param receiver The User who should recieve the message.
* @param text The Text of the message.
*/
public Message(MessageType type, String sender, String receiver, String text) {
this.type = type;
this.sender = sender;
this.receiver = receiver;
this.text = text;
}
/**
* Checks if the Filter String is contained in one of the datafields sender, receiver or text
* @param filter The Filter String
* @return true if it the Filter String is contained in a datafield.
*/
public boolean matchesFilter(String filter){
return (sender != null && sender.contains(filter)) ||
(receiver != null && receiver.contains(filter)) ||
(text != null && text.contains(filter));
}
/**
* @return The type of the message.
*/
public MessageType getType() {
return type;
}
/**
* @return The User who has sent the Message.
*/
public String getSender() {
return sender;
}
/**
* @return The Reciever who recieves the Message.
*/
public String getReceiver() {
return receiver;
}
/**
* @return The Text of the Message.
*/
public String getText() {
return text;
}
/**
* Enummeration of Message Types.
*/
public enum MessageType {
INFO, MESSAGE, ERROR;
}
}

View File

@ -1,20 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.*?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.*?>
<?import javafx.scene.control.Label?> <?import javafx.scene.layout.*?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:id="rootPane" minWidth="-Infinity" prefHeight="500.0" <BorderPane fx:id="rootPane" minWidth="-Infinity" prefHeight="500.0" prefWidth="420.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ch.zhaw.pm2.multichat.client.ChatWindowController">
prefWidth="420.0" xmlns="http://javafx.com/javafx/18"
fx:controller="ch.zhaw.pm2.multichat.client.ChatWindowController">
<top> <top>
<VBox BorderPane.alignment="CENTER"> <VBox BorderPane.alignment="CENTER">
<children> <children>
@ -24,7 +14,7 @@
<items> <items>
<MenuItem mnemonicParsing="false" text="press Enter"> <MenuItem mnemonicParsing="false" text="press Enter">
<graphic> <graphic>
<TextField fx:id="filterValue" onAction="#applyFilter"/> <TextField fx:id="filterValue" onAction="#applyFilter" />
</graphic> </graphic>
</MenuItem> </MenuItem>
</items> </items>
@ -33,35 +23,30 @@
</MenuBar> </MenuBar>
<HBox fillHeight="false" spacing="5.0"> <HBox fillHeight="false" spacing="5.0">
<children> <children>
<TextField fx:id="userNameField" alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308" <TextField fx:id="userNameField" alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308" minWidth="110.0" promptText="Username" HBox.hgrow="SOMETIMES" />
minWidth="110.0" promptText="Username" HBox.hgrow="SOMETIMES"/> <Label alignment="CENTER" contentDisplay="CENTER" text="\@" textAlignment="CENTER" textOverrun="CLIP" HBox.hgrow="NEVER">
<Label alignment="CENTER" contentDisplay="CENTER" text="\@" textAlignment="CENTER"
textOverrun="CLIP" HBox.hgrow="NEVER">
<HBox.margin> <HBox.margin>
<Insets bottom="5.0" top="5.0"/> <Insets bottom="5.0" top="5.0" />
</HBox.margin> </HBox.margin>
</Label> </Label>
<TextField fx:id="serverAddressField" alignment="CENTER_RIGHT" minWidth="110.0" <TextField fx:id="serverAddressField" alignment="CENTER_RIGHT" minWidth="110.0" promptText="Host" HBox.hgrow="SOMETIMES" />
promptText="Host" HBox.hgrow="SOMETIMES"/>
<Label text=":" HBox.hgrow="NEVER"> <Label text=":" HBox.hgrow="NEVER">
<HBox.margin> <HBox.margin>
<Insets bottom="5.0" top="5.0"/> <Insets bottom="5.0" top="5.0" />
</HBox.margin> </HBox.margin>
</Label> </Label>
<TextField fx:id="serverPortField" minWidth="-Infinity" prefWidth="60.0" promptText="Port" <TextField fx:id="serverPortField" minWidth="-Infinity" prefWidth="60.0" promptText="Port" HBox.hgrow="NEVER" />
HBox.hgrow="NEVER"/> <Button fx:id="connectButton" maxWidth="-Infinity" minWidth="-Infinity" mnemonicParsing="false" onAction="#toggleConnection" prefWidth="80.0" text="Connect" HBox.hgrow="NEVER">
<Button fx:id="connectButton" maxWidth="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
onAction="#toggleConnection" prefWidth="80.0" text="Connect" HBox.hgrow="NEVER">
<HBox.margin> <HBox.margin>
<Insets left="5.0"/> <Insets left="5.0" />
</HBox.margin> </HBox.margin>
</Button> </Button>
</children> </children>
<BorderPane.margin> <BorderPane.margin>
<Insets/> <Insets />
</BorderPane.margin> </BorderPane.margin>
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
</HBox> </HBox>
</children> </children>
@ -70,23 +55,22 @@
<bottom> <bottom>
<HBox spacing="5.0"> <HBox spacing="5.0">
<children> <children>
<TextField fx:id="messageField" onAction="#message" HBox.hgrow="ALWAYS"/> <TextField fx:id="messageField" onAction="#message" HBox.hgrow="ALWAYS" />
<Button fx:id="sendButton" alignment="CENTER" maxWidth="-Infinity" minWidth="-Infinity" <Button fx:id="sendButton" alignment="CENTER" maxWidth="-Infinity" minWidth="-Infinity" mnemonicParsing="false" onAction="#message" prefWidth="50.0" text="Send" textAlignment="CENTER">
mnemonicParsing="false" onAction="#message" prefWidth="50.0" text="Send" textAlignment="CENTER">
<HBox.margin> <HBox.margin>
<Insets left="5.0"/> <Insets left="5.0" />
</HBox.margin> </HBox.margin>
</Button> </Button>
</children> </children>
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
</HBox> </HBox>
</bottom> </bottom>
<center> <center>
<TextArea fx:id="messageArea" editable="false" wrapText="true"> <TextArea fx:id="messageArea" editable="false">
<BorderPane.margin> <BorderPane.margin>
<Insets left="5.0" right="5.0"/> <Insets left="5.0" right="5.0" />
</BorderPane.margin> </BorderPane.margin>
</TextArea> </TextArea>
</center> </center>

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2022-04-08T10:11:45.562Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/17.2.1 Chrome/96.0.4664.174 Electron/16.1.0 Safari/537.36" etag="zznHm94x3InQRhTWOwwy" version="17.2.1" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7Ztvc6o4FMY/jTPdF+4I+K8vRWvbu91uW/R2391JIWrWQFxIa+2nvycQQCRWqhfdbZnptOQQCJzfc0J4qjWj775e+mgx+5M5mNb0hvNaMwY1Xe90NPgtAqsooHVbzSgy9YkjY2nAIm9YBhsy+kwcHGQ6csYoJ4ts0Gaeh22eiSHfZ8tstwmj2VEXaIpzActGNB99JA6fRdGu3knjV5hMZ/HIWvs82uOiuLO8k2CGHLZcCxkXNaPvM8ajLfe1j6lIXpyXx+vVI72Zty+/3Qf/orH5x+j2ez062fAjhyS34GOP733qt/lkePW9+c/fi7vh/bJx2Xv6VpeHNF4QfZb5usV8yfz5FfIcin1553wVpzNYEpciD1rmhHncknsgGSaiZOrBtg1XCUca5gv2OQESPbmDswVE7Rmhzg1asWdxLwFH9jxumTPmkzc4LaKwS4MA7Pa5FJXezvSwxJEQbkDUxwH0uYsTpCWhGxRw2cdmlKJFQJ7CCxZdXORPiWcyzpkbn4g9ew52ZCshHja4z+aJhsTxBbFIfCIb+HVNlBLTJWYu5v4KusRFJ6nIkjNiAS5T/TYbMjZb066utWXdyJqZJqdORnuAGkPeFHKQDKcb2fG0Zn48ra0ab2M4RIG7hzg2RRaDdTXCxtqdpqFQox/Qq5bTa003BxfD3vhm9KM3GDxcWFbN6F17mPccBzQQ1DrmhHggqM4gJ2bgwdeES/GEb5VtsEA28aY3YZ9BM408yISIEINjJzSUzIw4DvZCSXHEUaQ6oaMFIx4PM9Yy4Qfy2m/83qq14IL60NbSNvyI7j7vMw/Uh0goMwySXmIh60wFNvNqfLfid6txlaW8U33viC8jg48y199jfvfXwwiAi9R8YtDvTEsz7lK5eRQ5tPQTy6GpkMMGbkrCB5RMjaacvHdowQWq4nQx/JHQxqCu5QRi5AViKMRA0ROmdywgnDBxfj/quyGSXTooC2qnWwxqtySmLVWJ2z6GZ4mFfbiPM5GVqM5/gz9ykRLtq+ltKpIzgo1pmqVPVP5lYe8ecWofz/SuaQ0pHd+a4/H1feeHO1csP2PuEvuXhZ2DWJT/VtiadsSZW0lbuXhjC+z1o3c/mBnPkFy1Zddwel9Z/+lxlQqKqsAoONWXpgLlcm5DBTMWcA+5sL9ncR9IVAr4dQpoN0+sAKOAAirGBzE+L2oZlLVK1zo5yBvP8WLGUqMyln6dsdRq7naWdKVMYkvqY85Sq/U/cpa6OcHWg1CqFrPnWDx6rPXmJ3Yb9vERkno/xFdSa680X0n1HKqchAOxKvwhJdayrAQ97w/VM0+ejJXw2Uq3NKztok+K0qo17xFtcK1oHmL8lEZT+QqgMnHNJSJ8yPzqLaDwW0BE/kOWz3E5K41dEvReEKEimSHhJ8YoRhXX4lwVJs5xubbVXPuUBdipoO4FVeHLHBdq/pUdoE4xv2IBv4OUVFz34qrwYo7LNf9mm3KVDnuINrZaK7LFyCaftzkZ2XMVWVtMwiHQFwZHVziL4myeerWk5/9Dtnv1Wxmnm8bpDm1s91K3GqfJHB6bHN0vaZyqRavnRFsPEsv0K5qlO/T3Tun/J8xS9fVVZmkJWI9olqovb7tZuubCbFTzZ/XZSuB7TNdUfX1KAybAnnPmQDaB6ehrLxT3oXpy91RX2i8+tjF5iVb+o4pmUZqnN0l1pfFSuaSHgj25S6ornZfKJj2I6sltUl3pukwxf8Au4zhxSkVOKqxFsZ7cJY2/BbcFqzBLv7xLuter68ldUkP5wfPKJt2TZ4k2KTTT71NHBlX6rXTj4ic=</diagram></mxfile>

View File

@ -1,101 +0,0 @@
package ch.zhaw.pm2.multichat.protocol;
import java.io.EOFException;
import java.io.IOException;
import java.net.SocketException;
/**
* This abstract class is the superclass for ClientConnectionHandler and ServerConnectionHandler
* It offers the DATA_TYPE for message and a {@link State} enum for all valid connection states.
* Shared methods are implemented in this class as well {@link ConnectionHandler#sendData(String, String, DATA_TYPE, String)}
*/
public abstract class ConnectionHandler {
private NetworkHandler.NetworkConnection<String> connection;
public static final String USER_NONE = "";
public static final String USER_ALL = "*";
// State of the connection
public enum State {
NEW, CONFIRM_CONNECT, CONNECTED, CONFIRM_DISCONNECT, DISCONNECTED, ERROR;
}
// DATA_TYPE of the messages
public enum DATA_TYPE {
DATA_TYPE_CONNECT, DATA_TYPE_CONFIRM, DATA_TYPE_DISCONNECT, DATA_TYPE_MESSAGE, DATA_TYPE_ERROR
}
/**
* @return {@link DATA_TYPE}
*/
public static DATA_TYPE getDataTypeConnect() {
return DATA_TYPE.DATA_TYPE_CONNECT;
}
/**
* @return {@link DATA_TYPE}
*/
public static DATA_TYPE getDataTypeConfirm() {
return DATA_TYPE.DATA_TYPE_CONFIRM;
}
/**
* @return {@link DATA_TYPE}
*/
public static DATA_TYPE getDataTypeDisconnect() {
return DATA_TYPE.DATA_TYPE_DISCONNECT;
}
/**
* @return {@link DATA_TYPE}
*/
public static DATA_TYPE getDataTypeMessage() {
return DATA_TYPE.DATA_TYPE_MESSAGE;
}
/**
* @return {@link DATA_TYPE}
*/
public static DATA_TYPE getDataTypeError() {
return DATA_TYPE.DATA_TYPE_ERROR;
}
/**
* @return {@link NetworkHandler.NetworkConnection}
*/
public NetworkHandler.NetworkConnection<String> getConnection() {
return connection;
}
/**
* This method sets the NetworkConnection used for the server <-> client connection
*
* @param connection NetworkConnection used for the server <-> client connection
*/
protected void setConnection(NetworkHandler.NetworkConnection<String> connection) {
this.connection = connection;
}
/**
* This method gets called to send data via the socket defined in the {@link NetworkHandler.NetworkConnection}
*
* @param sender of the data
* @param receiver of the data
* @param type of the data
* @param payload of the data
*/
protected void sendData(String sender, String receiver, DATA_TYPE type, String payload) {
if (connection.isAvailable()) {
try {
connection.send(new Message(type, sender, receiver, payload));
} catch (SocketException e) {
System.err.println("Connection closed: " + e.getMessage());
} catch (EOFException e) {
System.out.println("Connection terminated by remote");
} catch (IOException e) {
System.err.println("Communication error: " + e.getMessage());
}
}
}
}

View File

@ -1,69 +0,0 @@
package ch.zhaw.pm2.multichat.protocol;
import java.io.Serializable;
/**
* A Message object represents one Message of a client. Can be stored in ClientMessageList.
*/
public class Message implements Serializable {
private final ConnectionHandler.DATA_TYPE type;
private final String sender;
private final String receiver;
private final String text;
/**
* Constructor of Message. Needs all Information about a Message to save them.
*
* @param type Message (if it's a message typed by a user), Error or Information (if it is generated automatically, in this case sender and receiver will be null)
* @param sender The User who has sent the message.
* @param receiver The User who should receive the message.
* @param text The Text of the message.
*/
public Message(ConnectionHandler.DATA_TYPE type, String sender, String receiver, String text) {
this.type = type;
this.sender = sender;
this.receiver = receiver;
this.text = text;
}
/**
* Checks if the Filter String is contained in one of the data fields sender, receiver or text
*
* @param filter The Filter String
* @return true if it is the Filter String is contained in a data field.
*/
public boolean matchesFilter(String filter) {
return (sender != null && sender.contains(filter)) ||
(receiver != null && receiver.contains(filter)) ||
(text != null && text.contains(filter));
}
/**
* @return The type of the message.
*/
public ConnectionHandler.DATA_TYPE getType() {
return type;
}
/**
* @return The User who has sent the Message.
*/
public String getSender() {
return sender;
}
/**
* @return The Receiver who receives the Message.
*/
public String getReceiver() {
return receiver;
}
/**
* @return The Text of the Message.
*/
public String getText() {
return text;
}
}

View File

@ -38,7 +38,7 @@ import java.util.Objects;
* on the client side, usually the result is displayed to the user). After processing is finished the * on the client side, usually the result is displayed to the user). After processing is finished the
* process calls {@link NetworkConnection#receive()} again to wait for the next request. * process calls {@link NetworkConnection#receive()} again to wait for the next request.
* </li> * </li>
* <li>sending data: call {@link NetworkConnection#send(Message)}, which sends the given data * <li>sending data: call {@link NetworkConnection#send(Serializable data)}, which sends the given data
* object to the remote side. The method returns as soon the object has been transmitted. * object to the remote side. The method returns as soon the object has been transmitted.
* <b>Important: {@link NetworkConnection} is not thread safe</b>, therefore make sure that only one thread * <b>Important: {@link NetworkConnection} is not thread safe</b>, therefore make sure that only one thread
* at a time is sending data.</li> * at a time is sending data.</li>
@ -270,7 +270,7 @@ public class NetworkHandler {
* on the client side, usually the result is displayed to the user). After processing is finished the * on the client side, usually the result is displayed to the user). After processing is finished the
* process calls {@link NetworkConnection#receive()} again to wait for the next request. * process calls {@link NetworkConnection#receive()} again to wait for the next request.
* </li> * </li>
* <li>sending data: call {@link NetworkConnection#send(Message)}, which sends the given data * <li>sending data: call {@link NetworkConnection#send(Serializable data)}, which sends the given data
* object to the remote side. The method returns as soon the object has been transmitted. * object to the remote side. The method returns as soon the object has been transmitted.
* <b>Important: {@link NetworkConnection} is not thread safe</b>, therefore make sure that only one thread * <b>Important: {@link NetworkConnection} is not thread safe</b>, therefore make sure that only one thread
* at a time is sending data. * at a time is sending data.
@ -305,7 +305,7 @@ public class NetworkHandler {
* @param data data object of type T to be submitted through the connection. * @param data data object of type T to be submitted through the connection.
* @throws IOException if an error occurs (e.g. connection interrupted while sending, ...) * @throws IOException if an error occurs (e.g. connection interrupted while sending, ...)
*/ */
public synchronized void send(Message data) throws IOException { public synchronized void send(T data) throws IOException {
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream()); ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
outputStream.writeObject(data); outputStream.writeObject(data);
} }
@ -320,9 +320,9 @@ public class NetworkHandler {
* @throws IOException if an error occours. (e.g. terminated locally/remotely) see above. * @throws IOException if an error occours. (e.g. terminated locally/remotely) see above.
* @throws ClassNotFoundException if the data object received does not match any class in the local classpath * @throws ClassNotFoundException if the data object received does not match any class in the local classpath
*/ */
public Message receive() throws IOException, ClassNotFoundException { public T receive() throws IOException, ClassNotFoundException {
ObjectInputStream inputStream = new ObjectInputStream(this.socket.getInputStream()); ObjectInputStream inputStream = new ObjectInputStream(this.socket.getInputStream());
return (Message) inputStream.readObject(); return (T) inputStream.readObject();
} }
/** /**

File diff suppressed because one or more lines are too long

View File

@ -8,38 +8,14 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/**
* This Class represents a Server. The user can start the programm with the port number as a argument.
* If no argument has been set the {@link NetworkHandler#DEFAULT_PORT} is used as port number.
* After initialising the server:
* 1. Starts a Socketserver using the logic given in {@link NetworkHandler.NetworkServer#createServer()}
* 2. The server starts to listen for incoming connection using the Logic given in {@link NetworkHandler.NetworkServer#waitForConnection()}
* 3. New connections will be attached to a Connection handler: {@link ServerConnectionHandler} and placed in a Map containing all active connections {@link Server#connections}
*/
public class Server { public class Server {
// Server connection // Server connection
private final NetworkHandler.NetworkServer<String> networkServer; private NetworkHandler.NetworkServer<String> networkServer;
// Connection registry // Connection registry
private final Map<String, ServerConnectionHandler> connections = new HashMap<>(); private Map<String,ServerConnectionHandler> connections = new HashMap<>();
/**
* The Constructor to create a new instance.
*
* @param serverPort to listen for incoming connections.
* @throws IOException thrown if an I/O error occurs when opening the socket.
*/
public Server(int serverPort) throws IOException {
// Open server connection
System.out.println("Create server connection");
networkServer = NetworkHandler.createServer(serverPort);
System.out.println("Listening on " + networkServer.getHostAddress() + ":" + networkServer.getHostPort());
}
/**
* @param args
*/
public static void main(String[] args) { public static void main(String[] args) {
// Parse arguments for server port. // Parse arguments for server port.
try { try {
@ -78,30 +54,36 @@ public class Server {
} }
} }
/** public Server(int serverPort) throws IOException {
* With this methode the instance waits for incoming connections. If a client tries to connect to the server. // Open server connection
* The connection will be registered in the connection registry if successful. System.out.println("Create server connection");
*/ networkServer = NetworkHandler.createServer(serverPort);
System.out.println("Listening on " + networkServer.getHostAddress() + ":" + networkServer.getHostPort());
}
private void start() { private void start() {
System.out.println("Server started."); System.out.println("Server started.");
try { try {
while (networkServer.isAvailable()) { while (true) {
NetworkHandler.NetworkConnection<String> connection = networkServer.waitForConnection(); NetworkHandler.NetworkConnection<String> connection = networkServer.waitForConnection();
ServerConnectionHandler connectionHandler = new ServerConnectionHandler(connection, connections); ServerConnectionHandler connectionHandler = new ServerConnectionHandler(connection, connections);
new Thread(connectionHandler).start(); new Thread(connectionHandler).start();
System.out.println(String.format("Connected new Client %s with IP:Port <%s:%d>",
connectionHandler.getUserName(),
connection.getRemoteHost(),
connection.getRemotePort()
));
} }
} catch (SocketException e) { } catch(SocketException e) {
System.out.println("Server connection terminated"); System.out.println("Server connection terminated");
} catch (IOException e) { }
catch (IOException e) {
System.err.println("Communication error " + e); System.err.println("Communication error " + e);
} }
// close server // close server
System.out.println("Server Stopped."); System.out.println("Server Stopped.");
} }
/**
* This method will stop the serversocket.
*/
public void terminate() { public void terminate() {
try { try {
System.out.println("Close server port."); System.out.println("Close server port.");

View File

@ -1,92 +1,68 @@
package ch.zhaw.pm2.multichat.server; package ch.zhaw.pm2.multichat.server;
import ch.zhaw.pm2.multichat.protocol.ChatProtocolException; import ch.zhaw.pm2.multichat.protocol.ChatProtocolException;
import ch.zhaw.pm2.multichat.protocol.ConnectionHandler;
import static ch.zhaw.pm2.multichat.protocol.ConnectionHandler.State.*;
import ch.zhaw.pm2.multichat.protocol.Message;
import ch.zhaw.pm2.multichat.protocol.NetworkHandler; import ch.zhaw.pm2.multichat.protocol.NetworkHandler;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.net.SocketException; import java.net.SocketException;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import static ch.zhaw.pm2.multichat.server.ServerConnectionHandler.State.*;
/** public class ServerConnectionHandler implements Runnable{
* This class represents the connection between the server and a client and offers the serverside logic.
* The ServerConnectionHandler receives data send from the client as well as sends data to the client.
* <p>
* The ServeConnectionHandler offers following functionality:
* <p>
* Evaluating connection attempts from a client by:
* 1. Checks if Username is valid (Not used)
* 2. Saves Username in {@link ServerConnectionHandler#userName}
* 3. Saves the connection in the {@link ServerConnectionHandler#connectionRegistry}
* <p>
* Processes disconnections from a client by:
* 1. Removing the connection from the {@link ServerConnectionHandler#connectionRegistry}
* 2. Terminates the socket by calling {@link NetworkHandler.NetworkConnection#close()}
* <p>
* Process Messages send from a client by:
* 1. Evaluating the receiver by differentiating between broadcast or unicast.
* 2. Sending the message accordingly.
* <p>
* To use this class, start a new instance and start it in a thread.
* To constructor needs following parameter:
* 1. {@link ch.zhaw.pm2.multichat.protocol.NetworkHandler.NetworkConnection} representing the socket connection between server and client
* 2. {@link Map<String,ServerConnectionHandler>} registry to check for all active connections
* 3. {@link ReentrantLock @link Condition} to lock server thread to evaluate connection.
*/
public class ServerConnectionHandler extends ConnectionHandler implements Runnable {
private static final AtomicInteger connectionCounter = new AtomicInteger(0); private static final AtomicInteger connectionCounter = new AtomicInteger(0);
private final int connectionId = connectionCounter.incrementAndGet(); private final int connectionId = connectionCounter.incrementAndGet();
private final Map<String, ServerConnectionHandler> connectionRegistry; private final NetworkHandler.NetworkConnection<String> connection;
private final Map<String,ServerConnectionHandler> connectionRegistry;
private String userName = "Anonymous-" + connectionId; // Data types used for the Chat Protocol
private static final String DATA_TYPE_CONNECT = "CONNECT";
private static final String DATA_TYPE_CONFIRM = "CONFIRM";
private static final String DATA_TYPE_DISCONNECT = "DISCONNECT";
private static final String DATA_TYPE_MESSAGE = "MESSAGE";
private static final String DATA_TYPE_ERROR = "ERROR";
private static final String USER_NONE = "";
private static final String USER_ALL = "*";
private String userName = "Anonymous-"+connectionId;
private State state = NEW; private State state = NEW;
/**
* Called when runnable gets started in a thread.
*/
@Override @Override
public void run() { public void run() {
startReceiving(); startReceiving();
} }
/** enum State {
* Constructor to initialize the connection NEW, CONNECTED, DISCONNECTED;
* }
* @param connection representing the socket connection between server and client
* @param registry map containing all active connections between server and clients
*/
public ServerConnectionHandler(NetworkHandler.NetworkConnection<String> connection, public ServerConnectionHandler(NetworkHandler.NetworkConnection<String> connection,
Map<String, ServerConnectionHandler> registry) { Map<String,ServerConnectionHandler> registry) {
super();
setConnection(connection);
Objects.requireNonNull(connection, "Connection must not be null"); Objects.requireNonNull(connection, "Connection must not be null");
Objects.requireNonNull(registry, "Registry must not be null"); Objects.requireNonNull(registry, "Registry must not be null");
this.connection = connection;
this.connectionRegistry = registry; this.connectionRegistry = registry;
} }
/** public String getUserName() {
* These methods runs in a while-loop as long as the socket between server and client is available return this.userName;
* and the connection State is not ERROR. }
*/
private void startReceiving() { public void startReceiving() {
System.out.println("Starting Connection Handler for new User"); System.out.println("Starting Connection Handler for " + userName);
try { try {
System.out.println("Start receiving data..."); System.out.println("Start receiving data...");
while (getConnection().isAvailable() && !(state == ERROR)) { while (connection.isAvailable()) {
Message data = getConnection().receive(); String data = connection.receive();
processData(data); processData(data);
} }
System.out.println("Stopped receiving data"); System.out.println("Stopped recieving data");
} catch (SocketException e) { } catch (SocketException e) {
System.out.println("Connection terminated locally"); System.out.println("Connection terminated locally");
connectionRegistry.remove(userName); connectionRegistry.remove(userName);
@ -95,138 +71,117 @@ public class ServerConnectionHandler extends ConnectionHandler implements Runnab
System.out.println("Connection terminated by remote"); System.out.println("Connection terminated by remote");
connectionRegistry.remove(userName); connectionRegistry.remove(userName);
System.out.println("Unregistered because client connection terminated: " + userName + " " + e.getMessage()); System.out.println("Unregistered because client connection terminated: " + userName + " " + e.getMessage());
} catch (IOException e) { } catch(IOException e) {
System.err.println("Communication error: " + e); System.err.println("Communication error: " + e);
} catch (ClassNotFoundException e) { } catch(ClassNotFoundException e) {
System.err.println("Received object of unknown type: " + e.getMessage()); System.err.println("Received object of unknown type: " + e.getMessage());
} }
if (state == ERROR) {
System.out.println("Stopping Connection Handler for Rejected Client");
} else {
System.out.println("Stopping Connection Handler for " + userName); System.out.println("Stopping Connection Handler for " + userName);
} }
} public void stopReceiving() {
/**
* This method will call {@link NetworkHandler.NetworkConnection#close()} to close the Socket.
*/
private void stopReceiving() {
System.out.println("Closing Connection Handler for " + userName); System.out.println("Closing Connection Handler for " + userName);
try { try {
System.out.println("Stop receiving data..."); System.out.println("Stop receiving data...");
getConnection().close(); connection.close();
System.out.println("Stopped receiving data."); System.out.println("Stopped receiving data.");
} catch (IOException e) { } catch (IOException e) {
System.err.println("Failed to close connection." + e.getMessage()); System.err.println("Failed to close connection." + e);
} }
System.out.println("Closed Connection Handler for " + userName); System.out.println("Closed Connection Handler for " + userName);
} }
/** private void processData(String data) {
* This method gets called when socket receives data. The method checks for the data type and reacts accordingly
* If data type:
* 1. Connect => checks if username is valid. if valid sends response to client with confirmation.
* If username not valid quits connection by changing status to ERROR.
* 2. Confirm => Server should not receive this kind of message. STDOUT informs about it.
* 3. Disconnect => Disconnects connection by removing connection from registry and calling method to terminate socket.
* 4. Message => Checks if broadcast or unicast. Sends data accordingly
* 5. Error => STDERR message
*
* @param data received by the server
*/
private void processData(Message data) {
try { try {
// dispatch operation based on type parameter // parse data content
if (data.getType() == DATA_TYPE.DATA_TYPE_CONNECT) { Scanner scanner = new Scanner(data);
caseConnect(data); String sender = null;
} else if (data.getType() == DATA_TYPE.DATA_TYPE_CONFIRM) { String reciever = null;
System.out.println("Not expecting to receive a CONFIRM request from client"); String type = null;
} else if (data.getType() == DATA_TYPE.DATA_TYPE_DISCONNECT) { String payload = null;
caseDisconnect(); if (scanner.hasNextLine()) {
} else if (data.getType() == DATA_TYPE.DATA_TYPE_MESSAGE) { sender = scanner.nextLine();
caseMessage(data);
} else if (data.getType() == DATA_TYPE.DATA_TYPE_ERROR) {
System.err.println("Received error from client (" + data.getSender() + "): " + data.getText());
} else { } else {
System.err.println("Unknown data type received: " + data.getType()); throw new ChatProtocolException("No Sender found");
} }
} catch (ChatProtocolException e) { if (scanner.hasNextLine()) {
System.out.println("Error while processing data " + e.getMessage()); reciever = scanner.nextLine();
sendData(USER_NONE, userName, getDataTypeError(), e.getMessage()); } else {
throw new ChatProtocolException("No Reciever found");
} }
if (scanner.hasNextLine()) {
type = scanner.nextLine();
} else {
throw new ChatProtocolException("No Type found");
}
if (scanner.hasNextLine()) {
payload = scanner.nextLine();
} }
/** // dispatch operation based on type parameter
* This method is called by method {@link ServerConnectionHandler#processData(Message)} if (type.equals(DATA_TYPE_CONNECT)) {
* Checks if username is valid. if valid sends response to client with confirmation.
*
* @param data sent
* @throws ChatProtocolException if username not valid
*/
private void caseConnect(Message data) throws ChatProtocolException {
if (this.state != NEW) throw new ChatProtocolException("Illegal state for connect request: " + state); if (this.state != NEW) throw new ChatProtocolException("Illegal state for connect request: " + state);
//if username not valid if (sender == null || sender.isBlank()) sender = this.userName;
if (connectionRegistry.containsKey(data.getSender())) { if (connectionRegistry.containsKey(sender))
state = ERROR; throw new ChatProtocolException("User name already taken: " + sender);
System.out.println(String.format("Connecting failed for new Client with IP:Port <%s:%d>.\nReason: Name already taken.", this.userName = sender;
getConnection().getRemoteHost(),
getConnection().getRemotePort()));
throw new ChatProtocolException("User name already taken: " + data.getSender());
}
//if username valid
if (!data.getSender().isBlank()) {
this.userName = data.getSender();
}
connectionRegistry.put(userName, this); connectionRegistry.put(userName, this);
sendData(USER_NONE, userName, getDataTypeConfirm(), "Registration successful for " + userName); sendData(USER_NONE, userName, DATA_TYPE_CONFIRM, "Registration successfull for " + userName);
this.state = CONNECTED; this.state = CONNECTED;
System.out.println(String.format("Connected new Client %s with IP:Port <%s:%d>", } else if (type.equals(DATA_TYPE_CONFIRM)) {
userName, System.out.println("Not expecting to receive a CONFIRM request from client");
getConnection().getRemoteHost(), } else if (type.equals(DATA_TYPE_DISCONNECT)) {
getConnection().getRemotePort()));
}
/**
* This method is called by method {@link ServerConnectionHandler#processData(Message)}
* Disconnects connection by removing connection from registry and calling method {@link ServerConnectionHandler#stopReceiving()} to terminate socket.
*
* @throws ChatProtocolException if state already DISCONNECTED.
*/
private void caseDisconnect() throws ChatProtocolException {
if (state == DISCONNECTED) if (state == DISCONNECTED)
throw new ChatProtocolException("Illegal state for disconnect request: " + state); throw new ChatProtocolException("Illegal state for disconnect request: " + state);
if (state == CONNECTED) { if (state == CONNECTED) {
connectionRegistry.remove(this.userName); connectionRegistry.remove(this.userName);
} }
sendData(USER_NONE, userName, getDataTypeConfirm(), "Confirm disconnect of " + userName); sendData(USER_NONE, userName, DATA_TYPE_CONFIRM, "Confirm disconnect of " + userName);
this.state = DISCONNECTED; this.state = DISCONNECTED;
this.stopReceiving(); this.stopReceiving();
} else if (type.equals(DATA_TYPE_MESSAGE)) {
if (state != CONNECTED) throw new ChatProtocolException("Illegal state for message request: " + state);
if (USER_ALL.equals(reciever)) {
for (ServerConnectionHandler handler : connectionRegistry.values()) {
handler.sendData(sender, reciever, type, payload);
}
} else {
ServerConnectionHandler handler = connectionRegistry.get(reciever);
if (handler != null) {
handler.sendData(sender, reciever, type, payload);
} else {
this.sendData(USER_NONE, userName, DATA_TYPE_ERROR, "Unknown User: " + reciever);
}
}
} else if (type.equals(DATA_TYPE_ERROR)) {
System.err.println("Received error from client (" + sender + "): " + payload);
} else {
System.err.println("Unknown data type received: " + type);
}
} catch(ChatProtocolException e) {
System.out.println("Error while processing data" + e.getMessage());
sendData(USER_NONE, userName, DATA_TYPE_ERROR, e.getMessage());
}
} }
/** public void sendData(String sender, String receiver, String type, String payload) {
* This method is called by method {@link ServerConnectionHandler#processData(Message)} if (connection.isAvailable()) {
* Checks if broadcast or unicast. Sends data accordingly new StringBuilder();
* String data = new StringBuilder()
* @param data to transmit .append(sender+"\n")
* @throws ChatProtocolException if state not equal to CONNECT .append(receiver+"\n")
*/ .append(type+"\n")
private void caseMessage(Message data) throws ChatProtocolException { .append(payload+"\n")
if (state != CONNECTED) throw new ChatProtocolException("Illegal state for message request: " + state); .toString();
if (USER_ALL.equals(data.getReceiver())) { try {
for (ServerConnectionHandler handler : connectionRegistry.values()) { connection.send(data);
handler.sendData(data.getSender(), data.getReceiver(), data.getType(), data.getText()); } catch (SocketException e) {
} System.out.println("Connection closed: " + e.getMessage());
} else { } catch (EOFException e) {
ServerConnectionHandler handler = connectionRegistry.get(data.getReceiver()); System.out.println("Connection terminated by remote");
if (handler != null) { } catch(IOException e) {
handler.sendData(data.getSender(), data.getReceiver(), data.getType(), data.getText()); System.out.println("Communication error: " + e.getMessage());
if (!data.getReceiver().equals(data.getSender())) {
sendData(data.getSender(), data.getReceiver(), data.getType(), data.getText()); //send message to sender if it's a direct message and sender is not receiver.
}
} else {
this.sendData(USER_NONE, userName, getDataTypeError(), "Unknown User: " + data.getReceiver());
} }
} }
} }