Compare commits

...

84 Commits

Author SHA1 Message Date
Marcela Ruiz e437b35529 Feedback uebung ruiz
Feedback uebung ruiz
2022-05-06 16:51:12 +02:00
schrom01 7a5cd0afa8 corrections in Readme.md
removed old versions of classdiagrams.
2022-04-16 23:59:21 +02:00
schrom01 fed29883bc corrections in Readme.md 2022-04-16 23:52:20 +02:00
schrom01 fc6a91d3a8 corrections in Readme.md 2022-04-16 23:43:41 +02:00
schrom01 e460487550 Merge remote-tracking branch 'origin/main' 2022-04-16 23:38:43 +02:00
schrom01 9b993c3b93 added 5 structural problems in Readme.md 2022-04-16 23:38:32 +02:00
Andrin Fassbind 87782ae967 Klassendiagramm.svg 2022-04-16 23:28:33 +02:00
Andrin Fassbind 6a0a394ccc Klassendiagramm.svg 2022-04-16 23:19:18 +02:00
schrom01 9f3345c48c added 5 structural problems in Readme.md 2022-04-16 23:10:20 +02:00
schrom01 a0ceb0ad1c Merge remote-tracking branch 'origin/main' 2022-04-16 22:52:12 +02:00
schrom01 049bd399bd added 5 functional errors in Readme.md 2022-04-16 22:51:59 +02:00
Andrin Fassbind daf8e2bf95 Klassendiagramm.svgn 2022-04-16 22:46:39 +02:00
Roman Schenk a603f05135 Merge pull request #51 from PM2-IT21bWIN-ruiz-mach-krea/useDataObjectToSend
Use data object to send
2022-04-16 22:39:52 +02:00
fassband bfe703c7d8 Merge pull request #50 from PM2-IT21bWIN-ruiz-mach-krea/disabling_sendbutton
Solved #48 make it impossible to send messages if not connected.
2022-04-16 22:12:56 +02:00
Andrin Fassbind 40e3e14666 fixed #49
clean up
2022-04-16 22:09:56 +02:00
Andrin Fassbind a189fedd76 fixed #49
clean up
2022-04-16 22:01:59 +02:00
schrom01 af94f7ae00 added Description of Solution in Readme.md 2022-04-16 21:48:34 +02:00
Andrin Fassbind 6498b8ab33 fixed #49 2022-04-16 21:12:55 +02:00
Andrin Fassbind 77e9cf6163 Attempt to send obj 2022-04-16 21:04:51 +02:00
schrom01 11fdf8715e Solved #48 make it impossible to send messages if not connected in ChatWindow. 2022-04-16 20:27:29 +02:00
Roman Schenk b67b9c84aa Merge pull request #47 from PM2-IT21bWIN-ruiz-mach-krea/Java_Doc_and_Formatting
Java doc and formatting
2022-04-16 20:06:03 +02:00
Roman Schenk a678635355 Merge branch 'main' into Java_Doc_and_Formatting 2022-04-16 20:05:45 +02:00
schrom01 ff916daa51 Merge remote-tracking branch 'origin/Java_Doc_and_Formatting' into Java_Doc_and_Formatting 2022-04-16 20:02:16 +02:00
schrom01 61feb48a86 fixed Problem in processData in ConnectionHandler.java
and Codecleanup
2022-04-16 20:02:04 +02:00
Andrin Fassbind b4b91ea513 klassendiagramm 2022-04-16 19:53:15 +02:00
Andrin Fassbind dad15d52d2 javadoc 2022-04-16 19:40:51 +02:00
Andrin Fassbind 8395a121bb Klassendiagramm 2022-04-16 19:21:30 +02:00
Leonardo Brandenberger 8cb83ef7ef Fixed whitespaces ChatWindow.fxml other javadocs in other classes aswell deleted unneccessary declarations 2022-04-16 16:58:37 +02:00
Leonardo Brandenberger 154b9d435d Further improved Java Docs 10 missing todos. 2022-04-16 16:23:25 +02:00
Leonardo Brandenberger 048dc36a7c Merge branch 'main' into Java_Doc_and_Formatting
# Conflicts:
#	client/src/main/java/ch/zhaw/pm2/multichat/client/ChatWindowController.java
#	client/src/main/java/ch/zhaw/pm2/multichat/client/ClientConnectionHandler.java
#	protocol/src/main/java/ch/zhaw/pm2/multichat/protocol/ConnectionHandler.java
#	server/src/main/java/ch/zhaw/pm2/multichat/server/Server.java
#	server/src/main/java/ch/zhaw/pm2/multichat/server/ServerConnectionHandler.java
2022-04-16 15:59:21 +02:00
Leonardo Brandenberger c732e82260 General Codestyle and higlighted todos 2022-04-16 08:19:44 +02:00
Leonardo Brandenberger 2acfa708e9 ClientUI JavaDocs finished 2022-04-16 07:10:03 +02:00
Leonardo Brandenberger 7c8bdb3b33 improved java doc in ChatWindowController and Client class finished Client Connection Handler in Progress 2022-04-16 06:36:08 +02:00
Leonardo Brandenberger 4edcc92649 improved java doc in ChatWindowController and Client class finished 2022-04-16 06:36:08 +02:00
Leonardo Brandenberger dcee52cc1b Started Java Doc in ChatWindowController 2022-04-16 06:36:07 +02:00
Leonardo Brandenberger 6d6b9f1564 Java Doc and CodeStyle improvements 2022-04-16 06:36:05 +02:00
Andrin Fassbind c9aed1affd javadoc 2022-04-16 06:36:05 +02:00
schrom01 1d9beb6b7d fixed Problem setting Username in ClientConnectionHandler
#42
2022-04-16 06:36:05 +02:00
Andrin Fassbind add410250f fixed issue #39 2022-04-16 06:36:05 +02:00
Andrin Fassbind 5c0e55870d Java Doc Server && ServerConnectionHandler 2022-04-16 06:36:05 +02:00
Andrin Fassbind a94a4c81d8 Shrink code in the ConnectionHandlers 2022-04-16 06:36:05 +02:00
Andrin Fassbind 5022974427 Shrink code in the ConnectionHandlers 2022-04-16 06:36:04 +02:00
schrom01 46b0e4386a sovled #42 2022-04-16 06:36:02 +02:00
Andrin Fassbind a5069e938f javadoc 2022-04-15 22:06:22 +02:00
fassband 119cd1eb52 Merge pull request #46 from PM2-IT21bWIN-ruiz-mach-krea/refactoring_chatwindowcontroller
fixed Problem setting Username in ClientConnectionHandler
2022-04-15 21:40:36 +02:00
schrom01 e53a751081 fixed Problem setting Username in ClientConnectionHandler
#42
2022-04-15 21:20:03 +02:00
Roman Schenk 64a6137372 Merge pull request #45 from PM2-IT21bWIN-ruiz-mach-krea/remove_server_lock
fixed issue #39
2022-04-15 21:18:13 +02:00
Andrin Fassbind 5257195d82 fixed issue #39 2022-04-15 21:13:28 +02:00
Andrin Fassbind 73dcfb5058 Java Doc Server && ServerConnectionHandler 2022-04-15 19:41:43 +02:00
Roman Schenk 84e59effec Merge pull request #44 from PM2-IT21bWIN-ruiz-mach-krea/continue_connectionhandler
Continue connectionhandler
2022-04-15 17:41:12 +02:00
Leonardo Brandenberger 5f6266c017 Java Doc and CodeStyle improvements 2022-04-15 17:10:24 +02:00
schrom01 77b3057911 removed Method initialize because it's not used anymore. 2022-04-15 17:10:24 +02:00
schrom01 9e027b5c7a sovled #42 2022-04-15 17:10:23 +02:00
Andrin Fassbind 9a534b2465 Shrink code in the ConnectionHandlers 2022-04-15 16:39:52 +02:00
Andrin Fassbind 37045ade15 Shrink code in the ConnectionHandlers 2022-04-15 16:25:45 +02:00
Leonardo Brandenberger 313a4f5861 Merge pull request #43 from PM2-IT21bWIN-ruiz-mach-krea/refactoring_chatwindowcontroller
Refactoring chatwindowcontroller to improve MVC
2022-04-15 15:39:47 +02:00
schrom01 2b70299507 removed Method initialize because it's not used anymore. 2022-04-15 15:35:04 +02:00
schrom01 2b01e92e8d sovled #42 2022-04-15 15:34:03 +02:00
Leonardo Brandenberger 28376f1242 trying to fix MVC 2022-04-15 04:57:46 +02:00
Roman Schenk a75d3466ef Merge pull request #41 from PM2-IT21bWIN-ruiz-mach-krea/Server_Console_Display
Fixes issue #37
2022-04-15 01:07:22 +02:00
Leonardo Brandenberger 97b6fff82e fixes #37 2022-04-15 00:14:22 +02:00
fassband 2a8b701f48 Merge pull request #40 from PM2-IT21bWIN-ruiz-mach-krea/Refactoring_ClientConnectionHandler
Refactoring client connection handler
2022-04-14 22:24:13 +02:00
schrom01 86aa801b34 removed Enum State from ServerConnectionHandler.java because it's in ConnectionHandler.java now. 2022-04-14 22:23:49 +02:00
schrom01 2277fee73a merging 2022-04-14 22:12:35 +02:00
schrom01 d8dbd93c15 Merge branch 'main' into Refactoring_ClientConnectionHandler
# Conflicts:
#	client/src/main/java/ch/zhaw/pm2/multichat/client/ChatWindowController.java
#	client/src/main/java/ch/zhaw/pm2/multichat/client/ClientConnectionHandler.java
2022-04-14 22:02:39 +02:00
Roman Schenk 6e8e560d73 Merge pull request #38 from PM2-IT21bWIN-ruiz-mach-krea/ConnectionHandlerClass
Connection handler class
2022-04-14 21:51:59 +02:00
Andrin Fassbind 5a26c6b127 create setConnection in ConnectionHandlerClass 2022-04-14 21:37:06 +02:00
schrom01 615b3844e3 refactoring of ClientConnectionHandler
Instance is now created in ClientUI.java
solved #24 and #15
2022-04-14 21:13:36 +02:00
Andrin Fassbind 5e6e9d5817 Create ConnectionHandlerClass
Remove Codeduplication
2022-04-14 21:11:58 +02:00
Roman Schenk 4a998b0f61 Merge pull request #36 from PM2-IT21bWIN-ruiz-mach-krea/SpecialCharacterUsername
Special character username
2022-04-14 20:39:05 +02:00
fassband af46e499ff Merge pull request #34 from PM2-IT21bWIN-ruiz-mach-krea/Fix_WindowCloseHandler
fixed Disconnecting when window is closed
2022-04-14 19:36:26 +02:00
Andrin Fassbind b6fd5b569d fixed Issue #28 by checking if username contains space before starting connection 2022-04-14 19:33:18 +02:00
schrom01 8c588ee75c fixed Disconnecting when window is closed
#6
2022-04-14 19:13:21 +02:00
Roman Schenk 800528cc37 Merge pull request #33 from PM2-IT21bWIN-ruiz-mach-krea/Server_Console_Display
fixes Issue #20
2022-04-14 16:02:06 +02:00
Andrin Fassbind b47d98b960 fixed Issue #19 by changing messagePattern 2022-04-14 12:11:03 +02:00
Leonardo Brandenberger 886d9e5e43 fixes #20 2022-04-14 06:55:10 +02:00
fassband 7c68472859 Merge pull request #32 from PM2-IT21bWIN-ruiz-mach-krea/Fixing_private_Messages
fixed Problem "private Messages are visible for sender"
2022-04-13 17:35:48 +02:00
Roman Schenk b9ffb2b133 Update ServerConnectionHandler.java
added Commment for documentation.
2022-04-13 17:35:07 +02:00
schrom01 e69ced4081 fixed Problem "private Messages are visible for sender"
#28
2022-04-13 17:24:22 +02:00
Roman Schenk d1dfe6c1ab Merge pull request #31 from PM2-IT21bWIN-ruiz-mach-krea/Remove_Controller_From_Handler
Remove ChatWindowController from ClientConnectionHandler
2022-04-13 16:13:33 +02:00
Andrin Fassbind 3eedf58685 Remove ChatWindowController from ClientConnectionHandler
Make ChatWindowController listen for message changes

fixed Issue #23
2022-04-13 15:55:08 +02:00
Andrin Fassbind 1777b62582 Make State in ClientConnectionHandler property
Add change listener for stateproperty in ChatWindowController

fixed Issue #21
2022-04-13 15:03:07 +02:00
Roman Schenk a0b7b363c3 Merge pull request #30 from PM2-IT21bWIN-ruiz-mach-krea/Server_Console_Display
fixed issue #5
2022-04-12 21:35:26 +02:00
Leonardo Brandenberger 914fa8f1e2 fixed #5 2022-04-12 16:42:19 +02:00
18 changed files with 989 additions and 504 deletions

BIN
Feedback_ruiz_uebung.md.pdf Normal file

Binary file not shown.

View File

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

4
classdiagramm.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -1 +0,0 @@
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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