Compare commits
No commits in common. "main" and "SpecialCharacterUsername" have entirely different histories.
main
...
SpecialCha
Binary file not shown.
81
README.md
81
README.md
|
@ -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 |
|
@ -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">7Z1bU+O4EoB/TaqyD0zFdm485sYMu8CwwNzOy5SIReLFsbK2Qsj8+pVs+Ra3EwcimcOoamqIZDmx1Z9aLanValijxfNHHy3nl8TGbsNs2c8Na9wwTcPoWOwPz9lEOT3TiDJmvmOLQmnGrfMLi8yWyF05Ng5yBSkhLnWW+cwp8Tw8pbk85PtknS/2QNz8ry7RDBcybqfILeZ+c2w6j3L7Zi/N/4Sd2Tz+ZaN7Gl1ZoLiweJNgjmyyzmRZk4Y18gmh0afF8wi7vPLievl2vvnmXjx2P/75d/Av+jL86+7q60n0ZWeH3JK8go89+uKvPv/ysXu/nJzcDOfj8b+P//v+V/vuxDDFu9FNXGHYZvUnksSnczIjHnInae7QJyvPxvxrWyyVlrkgZMkyDZb5D6Z0I2BAK0pY1pwuXHGVvYa/+S7uDxM/eOKD2YnT4+fs1fEmSdkDzgRLTl0UBM40yjxz3Pi7o/fhL7El/z2VJ8oFZOVPxa2/Hh/OPn1t//N9eX3297r1cXD/54mo5BZF/gzTHeWMXoIIa1uYLDB7EXajj11Enaf80yEB+SwpJ25lb4s2mQJL4ng0yHzzNc9gBUR7bffE84nW2t1iYk9xo9fPlmcfogeIU5k3SbNCzmDmdtXgE3JXohZGrsOFs01isHYWLvI4cg/EozGU7N2HyHVmHqeA3Yd9lvGEfeqwVj8QFyhHcTidO659gTZkxUUVUDR9jFPDOfGdX+xrUUwOu+xTwazZzZW45XcKBn0csDLXMU9GknWBAirKTInromXg3IcPzIssGC+ONySUkkX8RblmlGiXMEF98pjoqxTrqhTz2sDPGcSKHIqriXYTBMTJdaorDQFRa57Rk/3WHnIXzzdMnSNvxqog/TVri7d2xZ9j8sj9HHKZ2D1E8ZBXYrAF7aGYgqrRLHJaIDQvwvXcofh2iUL9sWbdaV7rlYgmL9XyFlMU4SZfN3vrsF0uslzlHdqgDaCihmEP6nhNxj37vcEt9R1v1ugMG53xHyzNyxP2rds1yuqGZtq3ix9oaesOWFWzL70Iy4zbac6NeGmeRdi9D27YsuaObWMvbHkUURQ1Ti44oVXZA/DnG7K6G7U+dNiTsg8sbaRp9o8X9+mIeKyRIieUG2Ytf415688pqvYh4q3UaIXEOy9sNUeTeLdEhY8iI84h3ifk2S7TzBV1ekvr9CPq9C0lC+DStgBcOqc7NES5Uje2lLrVfxNKHQS3VwD3JB17MGy5ZhqZjUHrCtM18R8Fxh9EMuW7YXZdTpzQa2Z3RkOJthq94YPDjOFGb/zulFtFfdatDGx5DwYCegx9Bvf1RYUmuGCN1uV6LOZiNEf0m+OxZj1KL75vqe/QacK6ab3Kuunu7utMdWyAj9cH0RgP7gY/735cT36OPl9dTUZ3kU0TqwMNhDRlIYao9SkLq72fiLPzm0tNhCIVYbTadSMB9x8pEuPzW60nFFNhVTR75VHR2UPF5eT2dvBxopFQhUSnbkPTggcgKRKTm5vPNxoIVUD0FVqXMBCnBSCiybMvt5Obn1efr7R2UAWD2ardsiyONTIwDC4uNAuqWLDqNinbxeUH3lOsAuxfoQXOk6Blf0zZd+s2HNsmKPuAVXgieP5Ry/2ocu8rtA7B54NsgS0Ru064dpJZViyuK+yR/4JJMvTjEAK/C702TowCFFYRCgsAwEX32L0mgRNNoI/9qOwWGHXNLVuxi8s+qfYlCdUoKvKoU4+WzCZP7D+xzNB88apDZgWDtToOqGWEbSWctOamwpv8kFgySbfGn75skj2djv/j3am+/GKorLYAeHeAbSE2Po7fs8G+AiEMM0zDbq35h+7jZPVxVu22TRdWhyEBgSCAXW7GHzy8Dj++Z6eRmplQaffATOzQCv7K4zho8csSf7uqgSRN/D14DixSCdyB5wZPsfPEhKQxkIeBVffsV7/YM5xEECx9MsVBMGY130xMRpulNA7ScOjWPQFmtHZaCp6dx4Hn8FHCKDtB6oeKo5BNN0u8lbVEG5cgWwMlDajTui1Po7XDzBADaN3ByHPkMuo2M41WcV41AcB2As2AdAbime0aGbDKGVgwIwPNcHNf/yHKaU6kcdKt2xi1ChJVsi8y2d547xK+OyGzuTHZDRlunPyQbJX80chsqizZNxm9jLxNkYZwa8nuioRtfFjsh22KLGxj7PQFLrFbh7k1fxk9l7hri4wj7Cowil490eT+l/MCRnoDjPwNMJ3WFg/ADpjEsXye2/39om2Nnfj7/w+2NRqQEbzFqKJtjWVbpMt3hYBCk7av0Sw6ekcrd2Gjai59hzWADWtGs3RV/n1bBQcJ9pU7GmFZy1p6N2Ff3WmyEqnFfURx96o2bWlzjUDLjpbW47/onlfJlKaL7Mliuw5XULFn39MplHf2lXt2YEEbBOn9hysA3Uyk9+uHGPk1dOvg45V5XP+2vToQs+D4Yq/cw8tyPTHf6JD+2aGZET1L/chcScfzPKFqOC9a6N7RvGHBIq88nH/dVB40k5fvxB3e3T5wDVfsxW9Wnhe2pwNG6C/uoK3W4R10lHWJnnMFD+22ZfXEFeIGtYGecVdH/DoaIJOunv6vrFnE/jYVq0qaJjTKxrX+yquy+BGZeyPiEmbKjj0SNRWm0rayDvRDPrxPPOp8d757lSZ9KHYPJH1LmvSBaY1oRHMZLWtcOEHlwc1vPG2pYHDTL66CmD3AZrKsHePk8uHNGw3cU9Kqir298A7i3hwhs3GMlgjgyAIQTN+FHh+/RZCeI67c7dIfB43vIGjlLfG2oZAtidsQ9kth+a2COdXNCRDYRzUn0Gb8yOlYrPtrUt4CKUDEH9WkFOeMTnIuIxqUtwAKFAhIMSmd4jSsIGW2cqKhlQ4hpx4MYMytGAwgRFRBzO98c68UyQIhfUDJHmF7b0nfsMOhsDCubpY0fqYc3t+e1tpbPbCHQbXhAE+1hXAg2xZkNHOjVGh7QmZ3g+y93dr5tX5wgb0SILgdeVPERX+YBNy171B85vBJHxwjHKRO0w/hFc2HPD5MwA9BtZ0LuRF20YKLLF6Cy6Qm3mqBfSRiWEQXZrli8fgoP2EX3nzvx+Onk6S4dx/wP+dXZ58rFIsDLe4vGcXfe9XC0RbWEjHoVrR9etLURKdo1cIWjl5OiBPKlhPa/fz0frdX1BrWKTC9f9rbYSy/k+WETuncn7BvrhHlT5FM6sRpPVB/lUrr7Iu9Vg3RePAsQaOVLh5MC4ebJIcCVDz8RNPxOjraxYlgmA55Zs++ieBgC4pda+oah9fh0CsO7xXj0IXDOa8j88clAd5WFd+Kl3SPIhOSJIxEfZTEdhewzkgIvQ7N4MTK8HTIs6MjUNmqkKcoSvuNAPvsnQe2zUYawZmD+fG2AoY7JvcoRxNxZCIA5wPVSqE4gZIj4ppVn8ZBEQ6Ah4FqBVE67IhDo2oYFA1BAScC1boB2oyaHWVoFhSxAPgNqGYBOm0sy8LAxyiHQpihSTgyCYCfgWoSSj2gxczUcEVp5ghTkdQkHJkE4Mwg1SSY5bajZ2sMVGBgAacFqcYAOnUwlHm0BP81ytdWgmwUgMOCVA8fihqhIGbtXXi4ZIFw+aBkpXkXdnd4FzoeqzcmsV9Yh6yUCkHtmr5bOghAy6XLxMjbT7iooEmQScJp/Yq+dGqAktnMxekitCZB5ko0EO9AtfVX6sGi4xgrAKBd/3xA6ZSxjmWthoF4HF4jA6XripoBNQyc1m4clp+bEscz1wDIA6Bj1m4T9kuXkvnoYBPt1GiyApoDiRwAgX1Uc1A6Lczkv3C4R3XBR1XrBplMAHFuVTOxY/4oPE96NEfeDNv6vEWFXHTjg2hq5GLHxtQA0y/C6STd1Re7oWguJHJh1m1MmuDxAJnjWbPeitnD1zLZmhCJhHTqtjbN+LjQnYRw78VmdEZL6s6owZAIBrDZTjUYO0yNTKyDF53YqIMOSCeo16rbWDVLAtPGBJ17D6SpkVCIhFW3nWoaO+JQMCQmvk98zYRKJjr126hQKK34HFcXI/8y9YzV8xsyWejXbo2WR2b0se2jdTb2lkZBHgp9o3b7Ewi9GIW1D+MV6Z5CORJW7QYlGKUoQULbk6qJiN1UayQCnr0QRFQeozpYj1HrAAg4EUE1QNAYtTzw2chFQbAn5BkUE2I7UFkE6Ty83IxumDyFpxC2MP+7i7qaopftkeWpUdF4jJ1fD5AlS/LQCtmYUexF55fExrzEfw==</diagram></mxfile>
|
|
@ -1,9 +1,8 @@
|
||||||
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.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
@ -16,74 +15,45 @@ 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;
|
}
|
||||||
@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();
|
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.getStateProperty().get() != CONNECTED) {
|
||||||
connect();
|
connect();
|
||||||
} else {
|
} else {
|
||||||
|
@ -91,22 +61,16 @@ public class ChatWindowController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
||||||
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 +83,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 +99,53 @@ 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(" ")) {
|
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,
|
||||||
|
messages);
|
||||||
new Thread(connectionHandler).start();
|
new Thread(connectionHandler).start();
|
||||||
|
|
||||||
|
//register Listener
|
||||||
|
startListener();
|
||||||
|
|
||||||
|
// register window close handler
|
||||||
rootPane.getScene().getWindow().addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler);
|
rootPane.getScene().getWindow().addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler);
|
||||||
} else {
|
} else {
|
||||||
addError("It is not allowed to have spaces in username!");
|
addError("It is not allowed to have spaces in username!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void terminateConnectionHandler() {
|
||||||
* Sets the state shown according to the state the method receives, if state indicates disconnected it will also inform the
|
// unregister window close handler
|
||||||
* Connection Handler and tell it to stop Receiving more messages.
|
rootPane.getScene().getWindow().removeEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, windowCloseHandler);
|
||||||
*
|
if (connectionHandler != null) {
|
||||||
* @param newState is the state that it should be set to.
|
connectionHandler.stopReceiving();
|
||||||
*/
|
connectionHandler = null;
|
||||||
public void refreshConnectionState(State newState) {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stateChanged(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 +155,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 +164,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,36 +173,27 @@ 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) {
|
public void addError(String message) {
|
||||||
messages.addMessage(new Message(ConnectionHandler.DATA_TYPE.DATA_TYPE_ERROR, null, null, message));
|
messages.addMessage(new Message(Message.MessageType.ERROR, null, null, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void redrawMessageList() {
|
||||||
* Nested Class in charge of Closing the window
|
this.messageArea.clear();
|
||||||
*/
|
Platform.runLater(() -> this.messageArea.setText(messages.getFilteredMessages(filterValue.getText().strip())));
|
||||||
class WindowCloseHandler implements EventHandler<WindowEvent> {
|
}
|
||||||
|
|
||||||
/**
|
class WindowCloseHandler implements EventHandler<WindowEvent> {
|
||||||
* @param event the event which occurred when Windows is closed
|
|
||||||
*/
|
|
||||||
public void handle(WindowEvent event) {
|
public void handle(WindowEvent event) {
|
||||||
applicationClose();
|
applicationClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void startListener() {
|
||||||
* Starts several new Listener for Connection Handler changes by using several observable properties.
|
|
||||||
*/
|
|
||||||
public void startConnectionHandlerListener() {
|
|
||||||
connectionHandler.getStateProperty().addListener(new ChangeListener<State>() {
|
connectionHandler.getStateProperty().addListener(new ChangeListener<State>() {
|
||||||
@Override
|
@Override
|
||||||
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
|
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
|
||||||
refreshConnectionState(newValue);
|
stateChanged(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -290,14 +219,11 @@ public class ChatWindowController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a new Listener for messages by using the observable Boolean.
|
|
||||||
*/
|
|
||||||
private void messageListener() {
|
private void messageListener() {
|
||||||
messages.getChangedProperty().addListener(new ChangeListener<Boolean>() {
|
messages.getChangedProperty().addListener(new ChangeListener<Boolean>() {
|
||||||
@Override
|
@Override
|
||||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||||
Platform.runLater(() -> messageArea.setText(messages.getFilteredMessages(filterValue.getText().strip())));
|
redrawMessageList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
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.SimpleIntegerProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
@ -15,118 +13,71 @@ 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.
|
|
||||||
* 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( "^(?:@(\\S*))?\\s*(.*)$" );
|
||||||
* Constructor initializes ConnectionHandler by Setting default values for the fields and stores the given messages itself.
|
|
||||||
*
|
private SimpleStringProperty userName;
|
||||||
* @param messages
|
private SimpleObjectProperty<State> state;
|
||||||
*/
|
private ClientMessageList messages;
|
||||||
public ClientConnectionHandler(ClientMessageList messages) {
|
private SimpleStringProperty serverAddress;
|
||||||
super();
|
private SimpleIntegerProperty serverPort;
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
NEW, CONFIRM_CONNECT, CONNECTED, CONFIRM_DISCONNECT, DISCONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientConnectionHandler(NetworkHandler.NetworkConnection<String> connection,
|
||||||
|
String userName,
|
||||||
|
ClientMessageList messages) {
|
||||||
|
this.connection = connection;
|
||||||
|
this.userName = new SimpleStringProperty((userName == null || userName.isBlank())? USER_NONE : userName);
|
||||||
this.messages = messages;
|
this.messages = messages;
|
||||||
state = new SimpleObjectProperty<>(State.NEW);
|
state = new SimpleObjectProperty<>(NEW);
|
||||||
serverAddress = new SimpleStringProperty(NetworkHandler.DEFAULT_ADDRESS.getCanonicalHostName());
|
serverAddress = new SimpleStringProperty();
|
||||||
serverPort = new SimpleIntegerProperty(NetworkHandler.DEFAULT_PORT);
|
serverPort = new SimpleIntegerProperty();
|
||||||
this.userName = new SimpleStringProperty(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public SimpleStringProperty getServerAddressProperty() { return serverAddress; }
|
||||||
* 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 SimpleIntegerProperty getServerPortProperty() { return serverPort; }
|
||||||
* 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 SimpleObjectProperty<State> getStateProperty() {
|
||||||
return this.state;
|
return this.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public SimpleStringProperty getUserNameProperty() { return userName; }
|
||||||
* Observable getter Method for the stored userName
|
|
||||||
*
|
|
||||||
* @return the stored userName
|
|
||||||
*/
|
|
||||||
public SimpleStringProperty getUserNameProperty() {
|
|
||||||
return userName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
public void setState (State newState) {
|
||||||
* Method which sets a new State.
|
|
||||||
*
|
|
||||||
* @param newState the state that will be set
|
|
||||||
*/
|
|
||||||
public void setState(State newState) {
|
|
||||||
state.set(newState);
|
state.set(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void run () {
|
||||||
* 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 +86,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,114 +106,110 @@ 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;
|
||||||
// dispatch operation based on type parameter
|
String type = null;
|
||||||
if (data.getType() == DATA_TYPE.DATA_TYPE_CONNECT) {
|
String payload = null;
|
||||||
System.err.println("Illegal connect request from server");
|
if (scanner.hasNextLine()) {
|
||||||
} else if (data.getType() == DATA_TYPE.DATA_TYPE_CONFIRM) {
|
sender = scanner.nextLine();
|
||||||
caseConfirm(data);
|
} else {
|
||||||
} else if (data.getType() == DATA_TYPE.DATA_TYPE_DISCONNECT) {
|
throw new ChatProtocolException("No Sender found");
|
||||||
caseDisconnect(data);
|
}
|
||||||
} else if (data.getType() == DATA_TYPE.DATA_TYPE_MESSAGE) {
|
if (scanner.hasNextLine()) {
|
||||||
caseMessage(data);
|
reciever = scanner.nextLine();
|
||||||
} else if (data.getType() == DATA_TYPE.DATA_TYPE_ERROR) {
|
} else {
|
||||||
caseError(data);
|
throw new ChatProtocolException("No Reciever found");
|
||||||
} else {
|
}
|
||||||
System.out.println("Unknown data type received: " + data.getType());
|
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.get() == CONFIRM_CONNECT) {
|
||||||
|
this.userName.set(reciever);
|
||||||
|
this.serverPort.set(connection.getRemotePort());
|
||||||
|
this.serverAddress.set(connection.getRemoteHost());
|
||||||
|
messages.addMessage(new Message(Message.MessageType.INFO,sender,reciever,payload));
|
||||||
|
System.out.println("CONFIRM: " + payload);
|
||||||
|
this.setState(CONNECTED);
|
||||||
|
} else if (state.get() == CONFIRM_DISCONNECT) {
|
||||||
|
messages.addMessage(new Message(Message.MessageType.INFO,sender,reciever,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.get() == DISCONNECTED) {
|
||||||
|
System.out.println("DISCONNECT: Already in disconnected: " + payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
messages.addMessage(new Message(Message.MessageType.INFO,sender,reciever,payload));
|
||||||
|
System.out.println("DISCONNECT: " + payload);
|
||||||
|
this.setState(DISCONNECTED);
|
||||||
|
} else if (type.equals(DATA_TYPE_MESSAGE)) {
|
||||||
|
if (state.get() != CONNECTED) {
|
||||||
|
System.out.println("MESSAGE: Illegal state " + state + " for message: " + payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
messages.addMessage(new Message(Message.MessageType.MESSAGE,sender,reciever,payload));
|
||||||
|
System.out.println("MESSAGE: From " + sender + " to " + reciever + ": "+ payload);
|
||||||
|
} else if (type.equals(DATA_TYPE_ERROR)) {
|
||||||
|
messages.addMessage(new Message(Message.MessageType.ERROR,sender,reciever,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.get(), DATA_TYPE_ERROR, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void caseConfirm(Message data) {
|
public void sendData(String sender, String receiver, String type, String payload) {
|
||||||
if (state.get() == CONFIRM_CONNECT) {
|
if (connection.isAvailable()) {
|
||||||
this.userName.set(data.getReceiver());
|
new StringBuilder();
|
||||||
this.serverPort.set(getConnection().getRemotePort());
|
String data = new StringBuilder()
|
||||||
this.serverAddress.set(getConnection().getRemoteHost());
|
.append(sender+"\n")
|
||||||
messages.addMessage(data);
|
.append(receiver+"\n")
|
||||||
System.out.println("CONFIRM: " + data.getText());
|
.append(type+"\n")
|
||||||
this.setState(CONNECTED);
|
.append(payload+"\n")
|
||||||
} else if (state.get() == CONFIRM_DISCONNECT) {
|
.toString();
|
||||||
messages.addMessage(data);
|
try {
|
||||||
System.out.println("CONFIRM: " + data.getText());
|
connection.send(data);
|
||||||
this.setState(DISCONNECTED);
|
} catch (SocketException e) {
|
||||||
} else {
|
System.err.println("Connection closed: " + e.getMessage());
|
||||||
System.err.println("Got unexpected confirm message: " + data.getText());
|
} 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 {
|
public void connect() throws ChatProtocolException {
|
||||||
if (state.get() != NEW) throw new ChatProtocolException("Illegal state for connect: " + state);
|
if (state.get() != NEW) throw new ChatProtocolException("Illegal state for connect: " + state);
|
||||||
this.sendData(userName.get(), USER_NONE, getDataTypeConnect(), null);
|
this.sendData(userName.get(), 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.get() != NEW && state.get() != CONNECTED) throw new ChatProtocolException("Illegal state for disconnect: " + state);
|
||||||
throw new ChatProtocolException("Illegal state for disconnect: " + state);
|
this.sendData(userName.get(), 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.get() != CONNECTED) throw new ChatProtocolException("Illegal state for message: " + state);
|
||||||
|
|
||||||
|
@ -273,14 +217,15 @@ public class ClientConnectionHandler extends ConnectionHandler implements Runnab
|
||||||
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.get(), receiver, DATA_TYPE_MESSAGE,message);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +1,41 @@
|
||||||
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 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);
|
private 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());
|
changed.set(!changed.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public String getFilteredMessages(String filter) {
|
||||||
* 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();
|
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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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());
|
changed.set(!changed.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public SimpleBooleanProperty getChangedProperty() { return changed; }
|
||||||
* Getter Method to check the current value of SimpleBooleanProperty changed.
|
|
||||||
*
|
|
||||||
* @return the current value of changed
|
|
||||||
*/
|
|
||||||
public SimpleBooleanProperty getChangedProperty() {
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
@ -51,7 +32,7 @@ public class ClientUI extends Application {
|
||||||
primaryStage.setTitle("Multichat Client");
|
primaryStage.setTitle("Multichat Client");
|
||||||
primaryStage.show();
|
primaryStage.show();
|
||||||
primaryStage.setMinWidth(primaryStage.getWidth()); //use automatically computed size as Minimum Size.
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,82 +12,74 @@
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?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/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ch.zhaw.pm2.multichat.client.ChatWindowController">
|
||||||
prefWidth="420.0" xmlns="http://javafx.com/javafx/18"
|
<top>
|
||||||
fx:controller="ch.zhaw.pm2.multichat.client.ChatWindowController">
|
<VBox BorderPane.alignment="CENTER">
|
||||||
<top>
|
<children>
|
||||||
<VBox BorderPane.alignment="CENTER">
|
<MenuBar>
|
||||||
<children>
|
<menus>
|
||||||
<MenuBar>
|
<Menu mnemonicParsing="false" text="Filter">
|
||||||
<menus>
|
<items>
|
||||||
<Menu mnemonicParsing="false" text="Filter">
|
<MenuItem mnemonicParsing="false" text="press Enter">
|
||||||
<items>
|
<graphic>
|
||||||
<MenuItem mnemonicParsing="false" text="press Enter">
|
<TextField fx:id="filterValue" onAction="#applyFilter" />
|
||||||
<graphic>
|
</graphic>
|
||||||
<TextField fx:id="filterValue" onAction="#applyFilter"/>
|
</MenuItem>
|
||||||
</graphic>
|
</items>
|
||||||
</MenuItem>
|
</Menu>
|
||||||
</items>
|
</menus>
|
||||||
</Menu>
|
</MenuBar>
|
||||||
</menus>
|
<HBox fillHeight="false" spacing="5.0">
|
||||||
</MenuBar>
|
<children>
|
||||||
<HBox fillHeight="false" spacing="5.0">
|
<TextField fx:id="userNameField" alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308" minWidth="110.0" promptText="Username" HBox.hgrow="SOMETIMES" />
|
||||||
<children>
|
<Label alignment="CENTER" contentDisplay="CENTER" text="\@" textAlignment="CENTER" textOverrun="CLIP" HBox.hgrow="NEVER">
|
||||||
<TextField fx:id="userNameField" alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308"
|
<HBox.margin>
|
||||||
minWidth="110.0" promptText="Username" HBox.hgrow="SOMETIMES"/>
|
<Insets bottom="5.0" top="5.0" />
|
||||||
<Label alignment="CENTER" contentDisplay="CENTER" text="\@" textAlignment="CENTER"
|
</HBox.margin>
|
||||||
textOverrun="CLIP" HBox.hgrow="NEVER">
|
</Label>
|
||||||
<HBox.margin>
|
<TextField fx:id="serverAddressField" alignment="CENTER_RIGHT" minWidth="110.0" promptText="Host" HBox.hgrow="SOMETIMES" />
|
||||||
<Insets bottom="5.0" top="5.0"/>
|
<Label text=":" HBox.hgrow="NEVER">
|
||||||
</HBox.margin>
|
<HBox.margin>
|
||||||
</Label>
|
<Insets bottom="5.0" top="5.0" />
|
||||||
<TextField fx:id="serverAddressField" alignment="CENTER_RIGHT" minWidth="110.0"
|
</HBox.margin>
|
||||||
promptText="Host" HBox.hgrow="SOMETIMES"/>
|
</Label>
|
||||||
<Label text=":" HBox.hgrow="NEVER">
|
<TextField fx:id="serverPortField" minWidth="-Infinity" prefWidth="60.0" promptText="Port" HBox.hgrow="NEVER" />
|
||||||
<HBox.margin>
|
<Button fx:id="connectButton" maxWidth="-Infinity" minWidth="-Infinity" mnemonicParsing="false" onAction="#toggleConnection" prefWidth="80.0" text="Connect" HBox.hgrow="NEVER">
|
||||||
<Insets bottom="5.0" top="5.0"/>
|
<HBox.margin>
|
||||||
</HBox.margin>
|
<Insets left="5.0" />
|
||||||
</Label>
|
</HBox.margin>
|
||||||
<TextField fx:id="serverPortField" minWidth="-Infinity" prefWidth="60.0" promptText="Port"
|
</Button>
|
||||||
HBox.hgrow="NEVER"/>
|
</children>
|
||||||
<Button fx:id="connectButton" maxWidth="-Infinity" minWidth="-Infinity" mnemonicParsing="false"
|
<BorderPane.margin>
|
||||||
onAction="#toggleConnection" prefWidth="80.0" text="Connect" HBox.hgrow="NEVER">
|
<Insets />
|
||||||
<HBox.margin>
|
</BorderPane.margin>
|
||||||
<Insets left="5.0"/>
|
<padding>
|
||||||
</HBox.margin>
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
</Button>
|
</padding>
|
||||||
</children>
|
</HBox>
|
||||||
<BorderPane.margin>
|
</children>
|
||||||
<Insets/>
|
</VBox>
|
||||||
</BorderPane.margin>
|
</top>
|
||||||
<padding>
|
<bottom>
|
||||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
|
<HBox spacing="5.0">
|
||||||
</padding>
|
<children>
|
||||||
</HBox>
|
<TextField fx:id="messageField" onAction="#message" HBox.hgrow="ALWAYS" />
|
||||||
</children>
|
<Button fx:id="sendButton" alignment="CENTER" maxWidth="-Infinity" minWidth="-Infinity" mnemonicParsing="false" onAction="#message" prefWidth="50.0" text="Send" textAlignment="CENTER">
|
||||||
</VBox>
|
<HBox.margin>
|
||||||
</top>
|
<Insets left="5.0" />
|
||||||
<bottom>
|
</HBox.margin>
|
||||||
<HBox spacing="5.0">
|
</Button>
|
||||||
<children>
|
</children>
|
||||||
<TextField fx:id="messageField" onAction="#message" HBox.hgrow="ALWAYS"/>
|
<padding>
|
||||||
<Button fx:id="sendButton" alignment="CENTER" maxWidth="-Infinity" minWidth="-Infinity"
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
mnemonicParsing="false" onAction="#message" prefWidth="50.0" text="Send" textAlignment="CENTER">
|
</padding>
|
||||||
<HBox.margin>
|
</HBox>
|
||||||
<Insets left="5.0"/>
|
</bottom>
|
||||||
</HBox.margin>
|
<center>
|
||||||
</Button>
|
<TextArea fx:id="messageArea" editable="false" wrapText="true">
|
||||||
</children>
|
<BorderPane.margin>
|
||||||
<padding>
|
<Insets left="5.0" right="5.0" />
|
||||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
|
</BorderPane.margin>
|
||||||
</padding>
|
</TextArea>
|
||||||
</HBox>
|
</center>
|
||||||
</bottom>
|
|
||||||
<center>
|
|
||||||
<TextArea fx:id="messageArea" editable="false" wrapText="true">
|
|
||||||
<BorderPane.margin>
|
|
||||||
<Insets left="5.0" right="5.0"/>
|
|
||||||
</BorderPane.margin>
|
|
||||||
</TextArea>
|
|
||||||
</center>
|
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
|
@ -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>
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
@ -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.");
|
||||||
|
|
|
@ -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 new User");
|
||||||
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,120 @@ 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 " + userName);
|
||||||
System.out.println("Stopping Connection Handler for Rejected Client");
|
|
||||||
} else {
|
|
||||||
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 {
|
||||||
|
// 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
|
// dispatch operation based on type parameter
|
||||||
if (data.getType() == DATA_TYPE.DATA_TYPE_CONNECT) {
|
if (type.equals(DATA_TYPE_CONNECT)) {
|
||||||
caseConnect(data);
|
if (this.state != NEW) throw new ChatProtocolException("Illegal state for connect request: " + state);
|
||||||
} else if (data.getType() == DATA_TYPE.DATA_TYPE_CONFIRM) {
|
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");
|
System.out.println("Not expecting to receive a CONFIRM request from client");
|
||||||
} else if (data.getType() == DATA_TYPE.DATA_TYPE_DISCONNECT) {
|
} else if (type.equals(DATA_TYPE_DISCONNECT)) {
|
||||||
caseDisconnect();
|
if (state == DISCONNECTED)
|
||||||
} else if (data.getType() == DATA_TYPE.DATA_TYPE_MESSAGE) {
|
throw new ChatProtocolException("Illegal state for disconnect request: " + state);
|
||||||
caseMessage(data);
|
if (state == CONNECTED) {
|
||||||
} else if (data.getType() == DATA_TYPE.DATA_TYPE_ERROR) {
|
connectionRegistry.remove(this.userName);
|
||||||
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.
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
if(!reciever.equals(sender)){
|
||||||
|
sendData(sender, reciever, type, payload); //send message to sender if it's a direct message and sender is not receiver.
|
||||||
|
}
|
||||||
|
} 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 {
|
} 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue