Compare commits

..

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

17 changed files with 496 additions and 1034 deletions

Binary file not shown.

View File

@ -1,79 +1,2 @@
# Übung Multichat
Uebung-hk1-Schrom01-Fassband-Brandleo
# 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.
# Uebung-hk1-Schrom01-Fassband-Brandleo
Übung Multichat

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;
import ch.zhaw.pm2.multichat.protocol.ConnectionHandler;
import ch.zhaw.pm2.multichat.protocol.ConnectionHandler.State;
import ch.zhaw.pm2.multichat.client.ClientConnectionHandler.State;
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.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@ -16,188 +13,135 @@ import javafx.scene.layout.Pane;
import javafx.stage.WindowEvent;
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 {
private final Pattern messagePattern = Pattern.compile( "^(?:@(\\w*))?\\s*(.*)$" );
private ClientConnectionHandler connectionHandler;
private ClientMessageList messages;
private final WindowCloseHandler windowCloseHandler = new WindowCloseHandler();
@FXML
private Pane rootPane;
@FXML
private TextField serverAddressField;
@FXML
private TextField serverPortField;
@FXML
private TextField userNameField;
@FXML
private TextField messageField;
@FXML
private TextArea messageArea;
@FXML
private Button connectButton;
@FXML
private TextField filterValue;
@FXML
private Button sendButton;
@FXML private Pane rootPane;
@FXML private TextField serverAddressField;
@FXML private TextField serverPortField;
@FXML private TextField userNameField;
@FXML private TextField messageField;
@FXML private TextArea messageArea;
@FXML private Button connectButton;
@FXML private Button sendButton;
@FXML private TextField filterValue;
/**
* 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) {
this.messages = messages;
messageListener();
@FXML
public void initialize() {
serverAddressField.setText(NetworkHandler.DEFAULT_ADDRESS.getCanonicalHostName());
serverPortField.setText(String.valueOf(NetworkHandler.DEFAULT_PORT));
stateChanged(NEW);
messages = new ClientMessageList(this);
}
/**
* 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() {
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
private void toggleConnection() {
if (connectionHandler == null || connectionHandler.getStateProperty().get() != CONNECTED) {
private void toggleConnection () {
if (connectionHandler == null || connectionHandler.getState() != CONNECTED) {
connect();
} else {
disconnect();
}
}
/**
* Initiates a connection by starting Connection Handler and telling, the Handler to start a connection.
*/
private void connect() {
try {
messages.clear(); // clear message list
messages = new ClientMessageList(this); // clear message list
startConnectionHandler();
connectionHandler.connect();
} catch (ChatProtocolException | IOException e) {
addError("Error while starting Connection Handler and connect" + e);
} catch(ChatProtocolException | IOException e) {
writeError(e.getMessage());
}
}
/**
* Initiates disconnecting of the connectionHandler, also checks if connectionHandler is available.
*/
private void disconnect() {
if (connectionHandler == null) {
addError("No connection handler");
writeError("No connection handler");
return;
}
try {
connectionHandler.disconnect();
} catch (ChatProtocolException e) {
addError(e.getMessage());
writeError(e.getMessage());
}
}
/**
* Method which is used when the send button is pressed and handing over a message to the Connection Handler
*/
@FXML
private void message() {
if (connectionHandler == null) {
writeError("No connection handler");
return;
}
String messageString = messageField.getText().strip();
try {
if (connectionHandler == null) {
addError("No connection handler");
} else if (!connectionHandler.message(messageString)) {
addError("Not a valid message format.");
} else {
messageField.clear();
messageField.clear();
Matcher matcher = messagePattern.matcher(messageString);
if (matcher.find()) {
String receiver = matcher.group(1);
String message = matcher.group(2);
if (receiver == null || receiver.isBlank()) receiver = ClientConnectionHandler.USER_ALL;
try {
connectionHandler.message(receiver, message);
} catch (ChatProtocolException e) {
writeError(e.getMessage());
}
} catch (ChatProtocolException e) {
addError(e.getMessage());
} else {
writeError("Not a valid message format.");
}
}
/**
* Method which is used when a Filter is applied
* Setting the Text in the message area after sending it through the filter.
*/
@FXML
private void applyFilter() {
Platform.runLater(() -> this.messageArea.setText(messages.getFilteredMessages(filterValue.getText().strip())));
private void applyFilter( ) {
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 {
String userName = userNameField.getText();
if (!userName.contains(" ")) {
String serverAddress = serverAddressField.getText();
int serverPort = Integer.parseInt(serverPortField.getText());
connectionHandler.initialize(serverAddress, serverPort, userName);
new Thread(connectionHandler).start();
String serverAddress = serverAddressField.getText();
int serverPort = Integer.parseInt(serverPortField.getText());
connectionHandler = new ClientConnectionHandler(
NetworkHandler.openConnection(serverAddress, serverPort), userName,
this);
new Thread(connectionHandler).start();
rootPane.getScene().getWindow().addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler);
} else {
addError("It is not allowed to have spaces in username!");
// register window close handler
rootPane.getScene().getWindow().addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler);
}
private void terminateConnectionHandler() {
// unregister window close handler
rootPane.getScene().getWindow().removeEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler);
if (connectionHandler != null) {
connectionHandler.stopReceiving();
connectionHandler = null;
}
}
/**
* 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) {
public void stateChanged(State newState) {
// update UI (need to be run in UI thread: see Platform.runLater())
Platform.runLater(new Runnable() {
@Override
public void run() {
if(newState == CONNECTED || newState == CONFIRM_DISCONNECT){
connectButton.setText("Disconnect");
messageField.setDisable(false);
sendButton.setDisable(false);
} else {
connectButton.setText("Connect");
messageField.setDisable(true);
sendButton.setDisable(true);
}
connectButton.setText((newState == CONNECTED || newState == CONFIRM_DISCONNECT) ? "Disconnect" : "Connect");
}
});
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) {
Platform.runLater(new Runnable() {
@Override
@ -207,11 +151,6 @@ public class ChatWindowController {
});
}
/**
* Sets displayed Server Address.
*
* @param serverAddress provided String that is set as server address.
*/
public void setServerAddress(String serverAddress) {
Platform.runLater(new Runnable() {
@Override
@ -221,11 +160,6 @@ public class ChatWindowController {
});
}
/**
* Sets displayed Server port.
*
* @param serverPort provided String that is set as server port.
*/
public void setServerPort(int serverPort) {
Platform.runLater(new Runnable() {
@Override
@ -235,71 +169,51 @@ public class ChatWindowController {
});
}
/**
* Method which adds an incoming String as an Error Message.
*
* @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 addMessage(String sender, String receiver, String message) {
messages.addMessage(ClientMessageList.MessageType.MESSAGE, sender, receiver, message);
this.redrawMessageList();
}
/**
* Nested Class in charge of Closing the window
*/
class WindowCloseHandler implements EventHandler<WindowEvent> {
public void addInfo(String message) {
messages.addMessage(ClientMessageList.MessageType.INFO, null, null, message);
this.redrawMessageList();
}
/**
* @param event the event which occurred when Windows is closed
*/
public void addError(String message) {
messages.addMessage(ClientMessageList.MessageType.ERROR, null, null, message);
this.redrawMessageList();
}
public void clearMessageArea() {
this.messageArea.clear();
}
private void redrawMessageList() {
Platform.runLater(() -> messages.writeFilteredMessages(filterValue.getText().strip()));
}
public void writeError(String message) {
this.messageArea.appendText(String.format("[ERROR] %s\n", message));
}
public void writeInfo(String message) {
this.messageArea.appendText(String.format("[INFO] %s\n", message));
}
public void writeMessage(String sender, String reciever, String message) {
this.messageArea.appendText(String.format("[%s -> %s] %s\n", sender, reciever, message));
}
class WindowCloseHandler implements EventHandler<WindowEvent> {
public void handle(WindowEvent event) {
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;
/**
* Client Main Class in charge of starting the UI only contains main method.
*/
public class Client {
/**
* Main method which launches the Client UI
*
* @param args no arguments needed
*/
public static void main(String[] args) {
// Start UI
System.out.println("Starting Client Application");

View File

@ -1,132 +1,66 @@
package ch.zhaw.pm2.multichat.client;
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 javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import java.io.EOFException;
import java.io.IOException;
import java.net.SocketException;
import java.util.Scanner;
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.*;
/**
* Client Connection Handler Class is used for the connection of the Client to a server
* it is used part as a Model storing properties like userName, state, serverAddress and Port.
* Also holds methods to
*/
public class ClientConnectionHandler extends ConnectionHandler implements Runnable {
public class ClientConnectionHandler implements Runnable {
private final NetworkHandler.NetworkConnection<String> connection;
private final ChatWindowController controller;
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;
private final SimpleObjectProperty<State> state;
private final ClientMessageList messages;
private final SimpleStringProperty serverAddress;
private final SimpleIntegerProperty serverPort;
public static final String USER_NONE = "";
public static final String USER_ALL = "*";
/**
* Constructor initializes ConnectionHandler by Setting default values for the fields and stores the given messages itself.
*
* @param messages
*/
public ClientConnectionHandler(ClientMessageList messages) {
super();
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);
private String userName = USER_NONE;
private State state = NEW;
enum State {
NEW, CONFIRM_CONNECT, CONNECTED, CONFIRM_DISCONNECT, DISCONNECTED;
}
/**
* Called to initialize the ClientConnectionHandler when trying to start a connection
*
* @param serverAddress to connect to
* @param serverPort to connect to
* @param userName to connect as
* @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 ClientConnectionHandler(NetworkHandler.NetworkConnection<String> connection,
String userName,
ChatWindowController controller) {
this.connection = connection;
this.userName = (userName == null || userName.isBlank())? USER_NONE : userName;
this.controller = controller;
}
/**
* 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() {
public State getState() {
return this.state;
}
/**
* Observable getter Method for the stored userName
*
* @return the stored userName
*/
public SimpleStringProperty getUserNameProperty() {
return userName;
public void setState (State newState) {
this.state = newState;
controller.stateChanged(newState);
}
/**
* 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() {
public void run () {
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() {
public void startReceiving() {
System.out.println("Starting Connection Handler");
try {
System.out.println("Start receiving data...");
while (getConnection().isAvailable()) {
Message data = getConnection().receive();
while (connection.isAvailable()) {
String data = connection.receive();
processData(data);
}
System.out.println("Stopped receiving data");
System.out.println("Stopped recieving data");
} catch (SocketException e) {
System.out.println("Connection terminated locally");
this.setState(DISCONNECTED);
@ -135,22 +69,19 @@ public class ClientConnectionHandler extends ConnectionHandler implements Runnab
System.out.println("Connection terminated by remote");
this.setState(DISCONNECTED);
System.err.println("Unregistered because connection terminated" + e.getMessage());
} catch (IOException e) {
} catch(IOException e) {
System.err.println("Communication error" + e);
} catch (ClassNotFoundException e) {
} catch(ClassNotFoundException e) {
System.err.println("Received object of unknown type" + e.getMessage());
}
System.out.println("Stopped Connection Handler");
}
/**
* Method which is used to stop receiving data, gets the current connection and closes it.
*/
public void stopReceiving() {
System.out.println("Closing Connection Handler to Server");
try {
System.out.println("Stop receiving data...");
getConnection().close();
connection.close();
System.out.println("Stopped receiving data.");
} catch (IOException e) {
System.err.println("Failed to close connection." + e.getMessage());
@ -158,129 +89,114 @@ public class ClientConnectionHandler extends ConnectionHandler implements Runnab
System.out.println("Closed Connection Handler to Server");
}
/**
* Method which processes data and determines its type then uses the corresponding method to process it.
*
* @param data that is received in a form of a String and then used depending on its determined cause.
*/
private void processData(Message data) {
// dispatch operation based on type parameter
if (data.getType() == DATA_TYPE.DATA_TYPE_CONNECT) {
System.err.println("Illegal connect request from server");
} else if (data.getType() == DATA_TYPE.DATA_TYPE_CONFIRM) {
caseConfirm(data);
} else if (data.getType() == DATA_TYPE.DATA_TYPE_DISCONNECT) {
caseDisconnect(data);
} else if (data.getType() == DATA_TYPE.DATA_TYPE_MESSAGE) {
caseMessage(data);
} else if (data.getType() == DATA_TYPE.DATA_TYPE_ERROR) {
caseError(data);
} else {
System.out.println("Unknown data type received: " + data.getType());
private void processData(String data) {
try {
// parse data content
Scanner scanner = new Scanner(data);
String sender = null;
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
if (type.equals(DATA_TYPE_CONNECT)) {
System.err.println("Illegal connect request from server");
} else if (type.equals(DATA_TYPE_CONFIRM)) {
if (state == CONFIRM_CONNECT) {
this.userName = reciever;
controller.setUserName(userName);
controller.setServerPort(connection.getRemotePort());
controller.setServerAddress(connection.getRemoteHost());
controller.addInfo(payload);
System.out.println("CONFIRM: " + payload);
this.setState(CONNECTED);
} else if (state == CONFIRM_DISCONNECT) {
controller.addInfo(payload);
System.out.println("CONFIRM: " + payload);
this.setState(DISCONNECTED);
} else {
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);
return;
}
controller.addInfo(payload);
System.out.println("DISCONNECT: " + payload);
this.setState(DISCONNECTED);
} else if (type.equals(DATA_TYPE_MESSAGE)) {
if (state != CONNECTED) {
System.out.println("MESSAGE: Illegal state " + state + " for message: " + payload);
return;
}
controller.addMessage(sender, reciever, payload);
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());
}
}
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);
} else if (state.get() == CONFIRM_DISCONNECT) {
messages.addMessage(data);
System.out.println("CONFIRM: " + data.getText());
this.setState(DISCONNECTED);
} else {
System.err.println("Got unexpected confirm message: " + data.getText());
public void sendData(String sender, String receiver, String type, String payload) {
if (connection.isAvailable()) {
new StringBuilder();
String data = new StringBuilder()
.append(sender+"\n")
.append(receiver+"\n")
.append(type+"\n")
.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());
}
}
}
/**
* 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;
}
messages.addMessage(data);
System.out.println("DISCONNECT: " + data.getText());
this.setState(DISCONNECTED);
}
/**
* 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;
}
messages.addMessage(data);
System.out.println("MESSAGE: From " + data.getSender() + " to " + data.getReceiver() + ": " + data.getText());
}
/**
* Stores the message as an error message and displays it as such as well.
*
* @param data Data which has been transmitted
*/
private void caseError(Message data) {
messages.addMessage(data);
System.out.println("ERROR: " + data.getText());
}
/**
* Send connect attempt to Server
*
* @throws ChatProtocolException Error that is thrown if the state is not set to NEW
*/
public void connect() throws ChatProtocolException {
if (state.get() != NEW) throw new ChatProtocolException("Illegal state for connect: " + state);
this.sendData(userName.get(), USER_NONE, getDataTypeConnect(), null);
if (state != NEW) throw new ChatProtocolException("Illegal state for connect: " + state);
this.sendData(userName, USER_NONE, DATA_TYPE_CONNECT,null);
this.setState(CONFIRM_CONNECT);
}
/**
* Send Disconnect attempt to server
*
* @throws ChatProtocolException Error tha si thrown if state is invalid
*/
public void disconnect() throws ChatProtocolException {
if (state.get() != NEW && state.get() != CONNECTED)
throw new ChatProtocolException("Illegal state for disconnect: " + state);
this.sendData(userName.get(), USER_NONE, getDataTypeDisconnect(), null);
if (state != NEW && state != CONNECTED) throw new ChatProtocolException("Illegal state for disconnect: " + state);
this.sendData(userName, USER_NONE, DATA_TYPE_DISCONNECT,null);
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 {
if (state.get() != CONNECTED) throw new ChatProtocolException("Illegal state for message: " + state);
Matcher matcher = messagePattern.matcher(messageString);
if (matcher.find()) {
String receiver = matcher.group(1);
String message = matcher.group(2);
if (message.length() < 1) {
return false;
}
if (receiver == null || receiver.isBlank()) receiver = ClientConnectionHandler.USER_ALL;
this.sendData(userName.get(), receiver, getDataTypeMessage(), message);
return true;
} else {
return false;
}
public void message(String receiver, String message) throws ChatProtocolException {
if (state != CONNECTED) throw new ChatProtocolException("Illegal state for message: " + state);
this.sendData(userName, receiver, DATA_TYPE_MESSAGE,message);
}
}

View File

@ -1,68 +1,51 @@
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.List;
import java.util.Objects;
/**
* 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 {
private List<Message> messages = new ArrayList<>();
private final SimpleBooleanProperty changed = new SimpleBooleanProperty(false);
private final List<MessageType> typeList = new ArrayList<>();
private final List<String> senderList = new ArrayList<>();
private final List<String> receiverList = new ArrayList<>();
private final List<String> messageList = new ArrayList<>();
private final ChatWindowController gui;
/**
* Adds a new message to ArrayList and also informs Listener.
*
* @param message that should be added
*/
public void addMessage(Message message) {
messages.add(message);
changed.set(!changed.get());
public ClientMessageList(ChatWindowController gui) {
this.gui = gui;
}
public void addMessage(MessageType type, String sender, String receiver, String message) {
typeList.add(type);
senderList.add(sender);
receiverList.add(receiver);
messageList.add(message);
}
/**
* 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) {
StringBuilder result = new StringBuilder();
boolean showAll = filter == null || filter.isBlank();
for (Message message : messages) {
if (showAll || message.matchesFilter(filter)) {
switch (message.getType()) {
case DATA_TYPE_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 DATA_TYPE_CONFIRM, DATA_TYPE_DISCONNECT, DATA_TYPE_CONNECT -> result.append(String.format("[INFO] %s\n", message.getText()));
default -> result.append(String.format("[ERROR] %s\n", "Unexpected message type: " + message.getType()));
public void writeFilteredMessages(String filter) {
boolean showAll = filter == null || filter.isBlank();
gui.clearMessageArea();
for(int i=0; i<senderList.size(); i++) {
String sender = Objects.requireNonNullElse(senderList.get(i),"");
String receiver = Objects.requireNonNullElse(receiverList.get(i),"");
String message = Objects.requireNonNull(messageList.get(i), "");
if(showAll ||
sender.contains(filter) ||
receiver.contains(filter) ||
message.contains(filter))
{
switch (typeList.get(i)) {
case MESSAGE: gui.writeMessage(senderList.get(i), receiverList.get(i), messageList.get(i)); break;
case ERROR: gui.writeError(messageList.get(i)); break;
case INFO: gui.writeInfo(messageList.get(i)); break;
default: gui.writeError("Unexpected message type: " + typeList.get(i));
}
}
}
return result.toString();
}
}
}
}
/**
* Overwrites the Arraylist of messages, clearing it, also informs all Listeners.
*/
public void clear() {
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;
public enum MessageType {
INFO, MESSAGE, ERROR;
}
}

View File

@ -6,41 +6,17 @@ import javafx.scene.Scene;
import javafx.scene.layout.Pane;
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 {
private final 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
public void start(Stage 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) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("ChatWindow.fxml"));
Pane rootPane = loader.load();
ChatWindowController chatWindowController = loader.getController();
chatWindowController.setMessages(clientMessageList);
chatWindowController.setConnectionHandler(connectionHandler);
// fill in scene and stage setup
Scene scene = new Scene(rootPane);
//scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
@ -51,7 +27,7 @@ public class ClientUI extends Application {
primaryStage.setTitle("Multichat Client");
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());
}
}

View File

@ -12,82 +12,74 @@
<?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"
prefWidth="420.0" xmlns="http://javafx.com/javafx/18"
fx:controller="ch.zhaw.pm2.multichat.client.ChatWindowController">
<top>
<VBox BorderPane.alignment="CENTER">
<children>
<MenuBar>
<menus>
<Menu mnemonicParsing="false" text="Filter">
<items>
<MenuItem mnemonicParsing="false" text="press Enter">
<graphic>
<TextField fx:id="filterValue" onAction="#applyFilter"/>
</graphic>
</MenuItem>
</items>
</Menu>
</menus>
</MenuBar>
<HBox fillHeight="false" spacing="5.0">
<children>
<TextField fx:id="userNameField" alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308"
minWidth="110.0" promptText="Username" HBox.hgrow="SOMETIMES"/>
<Label alignment="CENTER" contentDisplay="CENTER" text="\@" textAlignment="CENTER"
textOverrun="CLIP" HBox.hgrow="NEVER">
<HBox.margin>
<Insets bottom="5.0" top="5.0"/>
</HBox.margin>
</Label>
<TextField fx:id="serverAddressField" alignment="CENTER_RIGHT" minWidth="110.0"
promptText="Host" HBox.hgrow="SOMETIMES"/>
<Label text=":" HBox.hgrow="NEVER">
<HBox.margin>
<Insets bottom="5.0" top="5.0"/>
</HBox.margin>
</Label>
<TextField fx:id="serverPortField" minWidth="-Infinity" prefWidth="60.0" promptText="Port"
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>
<Insets left="5.0"/>
</HBox.margin>
</Button>
</children>
<BorderPane.margin>
<Insets/>
</BorderPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</padding>
</HBox>
</children>
</VBox>
</top>
<bottom>
<HBox spacing="5.0">
<children>
<TextField fx:id="messageField" onAction="#message" HBox.hgrow="ALWAYS"/>
<Button fx:id="sendButton" alignment="CENTER" maxWidth="-Infinity" minWidth="-Infinity"
mnemonicParsing="false" onAction="#message" prefWidth="50.0" text="Send" textAlignment="CENTER">
<HBox.margin>
<Insets left="5.0"/>
</HBox.margin>
</Button>
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</padding>
</HBox>
</bottom>
<center>
<TextArea fx:id="messageArea" editable="false" wrapText="true">
<BorderPane.margin>
<Insets left="5.0" right="5.0"/>
</BorderPane.margin>
</TextArea>
</center>
<BorderPane fx:id="rootPane" minWidth="-Infinity" prefHeight="500.0" prefWidth="420.0" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ch.zhaw.pm2.multichat.client.ChatWindowController">
<top>
<VBox BorderPane.alignment="CENTER">
<children>
<MenuBar>
<menus>
<Menu mnemonicParsing="false" text="Filter">
<items>
<MenuItem mnemonicParsing="false" text="press Enter">
<graphic>
<TextField fx:id="filterValue" onAction="#applyFilter" />
</graphic>
</MenuItem>
</items>
</Menu>
</menus>
</MenuBar>
<HBox fillHeight="false" spacing="5.0">
<children>
<TextField fx:id="userNameField" alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308" minWidth="110.0" promptText="Username" HBox.hgrow="SOMETIMES" />
<Label alignment="CENTER" contentDisplay="CENTER" text="\@" textAlignment="CENTER" textOverrun="CLIP" HBox.hgrow="NEVER">
<HBox.margin>
<Insets bottom="5.0" top="5.0" />
</HBox.margin>
</Label>
<TextField fx:id="serverAddressField" alignment="CENTER_RIGHT" minWidth="110.0" promptText="Host" HBox.hgrow="SOMETIMES" />
<Label text=":" HBox.hgrow="NEVER">
<HBox.margin>
<Insets bottom="5.0" top="5.0" />
</HBox.margin>
</Label>
<TextField fx:id="serverPortField" minWidth="-Infinity" prefWidth="60.0" promptText="Port" 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>
<Insets left="5.0" />
</HBox.margin>
</Button>
</children>
<BorderPane.margin>
<Insets />
</BorderPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</HBox>
</children>
</VBox>
</top>
<bottom>
<HBox spacing="5.0">
<children>
<TextField fx:id="messageField" onAction="#message" HBox.hgrow="ALWAYS" />
<Button fx:id="sendButton" alignment="CENTER" maxWidth="-Infinity" minWidth="-Infinity" mnemonicParsing="false" onAction="#message" prefWidth="50.0" text="Send" textAlignment="CENTER">
<HBox.margin>
<Insets left="5.0" />
</HBox.margin>
</Button>
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</HBox>
</bottom>
<center>
<TextArea fx:id="messageArea" editable="false" wrapText="true">
<BorderPane.margin>
<Insets left="5.0" right="5.0" />
</BorderPane.margin>
</TextArea>
</center>
</BorderPane>

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
* process calls {@link NetworkConnection#receive()} again to wait for the next request.
* </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.
* <b>Important: {@link NetworkConnection} is not thread safe</b>, therefore make sure that only one thread
* 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
* process calls {@link NetworkConnection#receive()} again to wait for the next request.
* </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.
* <b>Important: {@link NetworkConnection} is not thread safe</b>, therefore make sure that only one thread
* 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.
* @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());
outputStream.writeObject(data);
}
@ -320,9 +320,9 @@ public class NetworkHandler {
* @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
*/
public Message receive() throws IOException, ClassNotFoundException {
public T receive() throws IOException, ClassNotFoundException {
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;
/**
* 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 {
// Server connection
private final NetworkHandler.NetworkServer<String> networkServer;
private NetworkHandler.NetworkServer<String> networkServer;
// 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) {
// Parse arguments for server port.
try {
@ -78,30 +54,36 @@ public class Server {
}
}
/**
* With this methode the instance waits for incoming connections. If a client tries to connect to the server.
* The connection will be registered in the connection registry if successful.
*/
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());
}
private void start() {
System.out.println("Server started.");
try {
while (networkServer.isAvailable()) {
NetworkHandler.NetworkConnection<String> connection = networkServer.waitForConnection();
ServerConnectionHandler connectionHandler = new ServerConnectionHandler(connection, connections);
new Thread(connectionHandler).start();
while (true) {
NetworkHandler.NetworkConnection<String> connection = networkServer.waitForConnection();
ServerConnectionHandler connectionHandler = new ServerConnectionHandler(connection, connections);
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");
} catch (IOException e) {
}
catch (IOException e) {
System.err.println("Communication error " + e);
}
// close server
System.out.println("Server Stopped.");
}
/**
* This method will stop the serversocket.
*/
public void terminate() {
try {
System.out.println("Close server port.");

View File

@ -1,92 +1,68 @@
package ch.zhaw.pm2.multichat.server;
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 java.io.EOFException;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import static ch.zhaw.pm2.multichat.server.ServerConnectionHandler.State.*;
/**
* 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 {
public class ServerConnectionHandler implements Runnable{
private static final AtomicInteger connectionCounter = new AtomicInteger(0);
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;
/**
* Called when runnable gets started in a thread.
*/
@Override
public void run() {
startReceiving();
}
/**
* Constructor to initialize the connection
*
* @param connection representing the socket connection between server and client
* @param registry map containing all active connections between server and clients
*/
enum State {
NEW, CONNECTED, DISCONNECTED;
}
public ServerConnectionHandler(NetworkHandler.NetworkConnection<String> connection,
Map<String, ServerConnectionHandler> registry) {
super();
setConnection(connection);
Map<String,ServerConnectionHandler> registry) {
Objects.requireNonNull(connection, "Connection must not be null");
Objects.requireNonNull(registry, "Registry must not be null");
this.connection = connection;
this.connectionRegistry = registry;
}
/**
* These methods runs in a while-loop as long as the socket between server and client is available
* and the connection State is not ERROR.
*/
private void startReceiving() {
System.out.println("Starting Connection Handler for new User");
public String getUserName() {
return this.userName;
}
public void startReceiving() {
System.out.println("Starting Connection Handler for " + userName);
try {
System.out.println("Start receiving data...");
while (getConnection().isAvailable() && !(state == ERROR)) {
Message data = getConnection().receive();
while (connection.isAvailable()) {
String data = connection.receive();
processData(data);
}
System.out.println("Stopped receiving data");
System.out.println("Stopped recieving data");
} catch (SocketException e) {
System.out.println("Connection terminated locally");
connectionRegistry.remove(userName);
@ -95,138 +71,117 @@ public class ServerConnectionHandler extends ConnectionHandler implements Runnab
System.out.println("Connection terminated by remote");
connectionRegistry.remove(userName);
System.out.println("Unregistered because client connection terminated: " + userName + " " + e.getMessage());
} catch (IOException e) {
} catch(IOException e) {
System.err.println("Communication error: " + e);
} catch (ClassNotFoundException e) {
} catch(ClassNotFoundException e) {
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);
}
/**
* This method will call {@link NetworkHandler.NetworkConnection#close()} to close the Socket.
*/
private void stopReceiving() {
public void stopReceiving() {
System.out.println("Closing Connection Handler for " + userName);
try {
System.out.println("Stop receiving data...");
getConnection().close();
connection.close();
System.out.println("Stopped receiving data.");
} 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);
}
/**
* 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) {
private void processData(String data) {
try {
// parse data content
Scanner scanner = new Scanner(data);
String sender = null;
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
if (data.getType() == DATA_TYPE.DATA_TYPE_CONNECT) {
caseConnect(data);
} else if (data.getType() == DATA_TYPE.DATA_TYPE_CONFIRM) {
if (type.equals(DATA_TYPE_CONNECT)) {
if (this.state != NEW) throw new ChatProtocolException("Illegal state for connect request: " + state);
if (sender == null || sender.isBlank()) sender = this.userName;
if (connectionRegistry.containsKey(sender))
throw new ChatProtocolException("User name already taken: " + sender);
this.userName = sender;
connectionRegistry.put(userName, this);
sendData(USER_NONE, userName, DATA_TYPE_CONFIRM, "Registration successfull for " + userName);
this.state = CONNECTED;
} else if (type.equals(DATA_TYPE_CONFIRM)) {
System.out.println("Not expecting to receive a CONFIRM request from client");
} else if (data.getType() == DATA_TYPE.DATA_TYPE_DISCONNECT) {
caseDisconnect();
} else if (data.getType() == DATA_TYPE.DATA_TYPE_MESSAGE) {
caseMessage(data);
} else if (data.getType() == DATA_TYPE.DATA_TYPE_ERROR) {
System.err.println("Received error from client (" + data.getSender() + "): " + data.getText());
} else {
System.err.println("Unknown data type received: " + data.getType());
}
} catch (ChatProtocolException e) {
System.out.println("Error while processing data " + e.getMessage());
sendData(USER_NONE, userName, getDataTypeError(), e.getMessage());
}
}
/**
* This method is called by method {@link ServerConnectionHandler#processData(Message)}
* 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 username not valid
if (connectionRegistry.containsKey(data.getSender())) {
state = ERROR;
System.out.println(String.format("Connecting failed for new Client with IP:Port <%s:%d>.\nReason: Name already taken.",
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);
sendData(USER_NONE, userName, getDataTypeConfirm(), "Registration successful for " + userName);
this.state = CONNECTED;
System.out.println(String.format("Connected new Client %s with IP:Port <%s:%d>",
userName,
getConnection().getRemoteHost(),
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)
throw new ChatProtocolException("Illegal state for disconnect request: " + state);
if (state == CONNECTED) {
connectionRegistry.remove(this.userName);
}
sendData(USER_NONE, userName, getDataTypeConfirm(), "Confirm disconnect of " + userName);
this.state = DISCONNECTED;
this.stopReceiving();
}
/**
* This method is called by method {@link ServerConnectionHandler#processData(Message)}
* Checks if broadcast or unicast. Sends data accordingly
*
* @param data to transmit
* @throws ChatProtocolException if state not equal to CONNECT
*/
private void caseMessage(Message data) throws ChatProtocolException {
if (state != CONNECTED) throw new ChatProtocolException("Illegal state for message request: " + state);
if (USER_ALL.equals(data.getReceiver())) {
for (ServerConnectionHandler handler : connectionRegistry.values()) {
handler.sendData(data.getSender(), data.getReceiver(), data.getType(), data.getText());
}
} else {
ServerConnectionHandler handler = connectionRegistry.get(data.getReceiver());
if (handler != null) {
handler.sendData(data.getSender(), data.getReceiver(), data.getType(), data.getText());
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 if (type.equals(DATA_TYPE_DISCONNECT)) {
if (state == DISCONNECTED)
throw new ChatProtocolException("Illegal state for disconnect request: " + state);
if (state == CONNECTED) {
connectionRegistry.remove(this.userName);
}
sendData(USER_NONE, userName, DATA_TYPE_CONFIRM, "Confirm disconnect of " + userName);
this.state = DISCONNECTED;
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 {
this.sendData(USER_NONE, userName, getDataTypeError(), "Unknown User: " + data.getReceiver());
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) {
if (connection.isAvailable()) {
new StringBuilder();
String data = new StringBuilder()
.append(sender+"\n")
.append(receiver+"\n")
.append(type+"\n")
.append(payload+"\n")
.toString();
try {
connection.send(data);
} catch (SocketException e) {
System.out.println("Connection closed: " + e.getMessage());
} catch (EOFException e) {
System.out.println("Connection terminated by remote");
} catch(IOException e) {
System.out.println("Communication error: " + e.getMessage());
}
}
}