diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f0da518 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Function is faulty or not as expected +title: '' +labels: 'fix' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Possible solution** +If you have an idea of how to fix the problem, describe it here. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..431bc54 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,8 @@ +--- +name: Blank +about: Create a blank issue +title: '' +labels: '' +assignees: '' + +--- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c76ac3b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Create a feature request +title: '' +labels: 'feat' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/refactor_report.md b/.github/ISSUE_TEMPLATE/refactor_report.md new file mode 100644 index 0000000..c6dd462 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor_report.md @@ -0,0 +1,24 @@ +--- +name: Refactor report +about: Bad structure, violated patterns, clean code, ... +title: '' +labels: 'refactor' +assignees: '' + +--- + +**Describe the refactor** +A clear and concise description of what the refactor is about. + +**Current code** +Include a link to the current code or provide a code snippet if it's a small refactor. + +**Possible refactor approach** +If you have an idea of how to refactor the code, describe it here. + +**Reason for the refactor** +A clear and concise description of why the refactor is needed. + +**Additional context** +Include any additional information or context that would be helpful for understanding the refactor +report. diff --git a/.github/assets/connection-registry.png b/.github/assets/connection-registry.png new file mode 100644 index 0000000..a5344af Binary files /dev/null and b/.github/assets/connection-registry.png differ diff --git a/.github/assets/connection.png b/.github/assets/connection.png new file mode 100644 index 0000000..9d31d12 Binary files /dev/null and b/.github/assets/connection.png differ diff --git a/.github/assets/execution.svg b/.github/assets/execution.svg new file mode 100644 index 0000000..b7f069f --- /dev/null +++ b/.github/assets/execution.svg @@ -0,0 +1,4 @@ + + + + diff --git a/.github/assets/logo.png b/.github/assets/logo.png new file mode 100644 index 0000000..56711f3 Binary files /dev/null and b/.github/assets/logo.png differ diff --git a/.github/assets/multichat_class_diagram.png b/.github/assets/multichat_class_diagram.png new file mode 100644 index 0000000..aa5d137 Binary files /dev/null and b/.github/assets/multichat_class_diagram.png differ diff --git a/.github/assets/mvc.png b/.github/assets/mvc.png new file mode 100644 index 0000000..d171658 Binary files /dev/null and b/.github/assets/mvc.png differ diff --git a/.github/assets/payload.png b/.github/assets/payload.png new file mode 100644 index 0000000..457f1d8 Binary files /dev/null and b/.github/assets/payload.png differ diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a581da6 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +# Resolves #000 + +## Description + +Please provide a brief description of the changes you've made in this pull request. + +## Checklist + +Please make sure that the following items have been completed before submitting this pull request: + +- [ ] All code has been properly tested +- [ ] All tests pass successfully +- [ ] Code has been reviewed for clarity, readability, and maintainability +- [ ] Code has been properly documented with JavaDoc comments diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e7c379 --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# InelliJ IDEA files +*.iml +*.ipr +*.ids +*.iws +.idea/ + +# Eclipse files +.project +.metadata +.classpath +.settings/ +.loadpath +bin/ + +# Netbeans +nbactions.xml + +# Visual Studio Code +.vscode + +# Maven +target/ + +# gradle files +.gradle +build/ + +# ignore logfiles +*.log* + +# OS dependant files +.DS_Store +.Spotlight-V100 +.Trashes +Thumbs.db +Desktop.ini +*~ +# Thumbnails +._* + +# compiled files +*.com +*.class +*.dll +*.exe +*.o +*.so + +# packages +*.7z +#*.jar +*.rar +*.zip +*.gz +*.bzip +*.xz +*.lzma +*~$* + +# package managment formats +*.dmg +*.xpi +*.gem +*.egg +*.deb +*.rpm + +# databases +*.sqlite diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4c142fe --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,119 @@ +# Code of Conduct + +## Conventional Commits + +Developers should use the Conventional Commits standard when committing changes to the codebase. + +| Type | Description | +|----------|-----------------------------------------------------------------------| +| feat | Declares a new feature has been added | +| fix | Declares a bug have been fixed | +| chore | Declares changes which don’t modify source or test files (eg. assets) | +| ci | Declares a change on the CI or CD process | +| build | Declares changes on the build setup | +| docs | Declares changes on documentation | +| style | Declares changes on code style | +| refactor | Declares a change of code without an effective change on the program | +| perf | Declares a change on performance | +| revert | Declares that a previous commit has been reverted | +| test | Declares changes on tests | + +### Examples + +#### Commit Message + +``` +refactor: adjust vehicle texture size [#ISSUENUMBER] +refactor: adjust vehicle texture size [NOISSUE] +``` + +#### Branch Name + +``` +refactor/#ISSUENUMBER_adjust-vehicle-texture-size +refactor/NOISSUE_adjust-vehicle-texture-size +``` + +## Contributing + +Developers should follow the following guidelines when contributing to the project: + +### 1. Create a new branch + +When starting work on a new feature or bug fix, create a new branch from the `main` branch. The name +of the branch should be descriptive and should include the issue number and a short description of +the feature or bug fix. For example, if you are working on issue #123, the branch name should +be `feat/#123_add-new-feature`. + +### 2. Commit changes + +When committing changes to the codebase, developers should follow +the [Conventional Commits](#conventional-commits) standard. This will ensure that the commit +messages are consistent and descriptive, and will allow the commit history to be automatically +parsed to generate release notes. + +### 3. Create a draft pull request + +After committing changes to the codebase, create a draft pull request to inform other developers +that you are working on a new feature or bug fix. The pull request should be kept in draft mode +until the feature or bug fix is complete. + +### 4. Create a pull request + +When the feature or bug fix is complete, mark the pull request as ready to review to merge the +changes into the `main` branch. The pull request should be reviewed by at least one other developer +before it can be merged. + +### 5. Review pull request + +When a pull request is marked as ready for review, it should be reviewed by at least one other +developer. The reviewer should verify that the code meets +the [Definition of Done](#definition-of-done). + +### 6. Merge pull request + +Once the pull request has been reviewed and approved, it can be merged into the `main` branch. The +pull request should be merged using the "Rebase and merge" option to ensure that the commit history +remains clean and concise. + +## Definition of Done + +### 1. Code meets coding standards + +All code must adhere to the rules defined in the Clean Code handbook for at least level L1. Level L2 +rules should also be taken into consideration. Specifically, emphasis should be placed on: + +1. Correct abstraction level: The code should have a clear and appropriate level of abstraction, + with well-defined interfaces and separation of concerns. +2. Class diagram: The class diagram should be clear and well-organized, with high cohesion and low + coupling between classes. +3. Correct error handling: The code should handle errors correctly, including validating arguments + and handling exceptions in a consistent and appropriate manner. + +### 2. Unit tests pass + +All code changes must be accompanied by unit tests that verify the expected behavior of the code. +These tests must pass without any errors or failures before the code can be considered complete. + +### 3. Code is reviewed + +All code must be reviewed by at least one other developer to ensure quality and compliance with +coding standards. The code review should focus on identifying any bugs, security vulnerabilities, or +design flaws that could impact the quality or maintainability of the code. + +### 4. Documentation is complete + +All code must be fully documented, including comments within the code and external documentation +such as user manuals. The documentation should be comprehensive and accurate, and should provide +enough detail for other developers and stakeholders to understand the code. + +### 5. Acceptance criteria are met + +The code must meet all of the acceptance criteria as defined by the stakeholders. These acceptance +criteria are used as a basis for verifying that the code meets the intended requirements + +### 6. Security is considered + +The code must be reviewed for security vulnerabilities and any identified issues must be addressed. +The code should be designed with security in mind, and should be subject to regular security testing +to identify any new vulnerabilities. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3ec4b16 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1 @@ +Copyright © 2023 Boostvolt (Jan). All rights reserved. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb1aab9 --- /dev/null +++ b/README.md @@ -0,0 +1,275 @@ + +
+Logo + +

Multichat

+ +

+ Chat more, type less! Let your fingers rest. +
+
+ Getting Started + · + Class Diagram + · + Application Structure + · + Issue List + · + GitHub Workflow +

+
+ +## Getting Started + +### Cloning the repository + +``` +git clone https://github.com/boostvolt/zhaw-multichat.git +``` + +### Start the program + +There are two ways of starting the program: + +#### Starting with IntelliJ: + +1. Make sure to have installed gradle version 8.0.1 or newer. +2. Open a command line prompt in the directory of the repository. Do this once for the server and as + many times as you want to have clients. Use the following commands to start the server or client: + +For the client + +```bash +$ ./gradlew client:run +``` + +For the server + +```bash +$ ./gradlew server:run +``` + +3. The program should compile and start accordingly. + +### Stop the program + +To stop the client just close the window. +To stop the client or the server close the window and press CTRL + C in the command line. + +

(back to top)

+ +## Class Diagram + +In the following class diagram we only included 1 PayloadHandler class on server and client side for +better readability. + +![multichat_class_diagram.png](.github%2Fassets%2Fmultichat_class_diagram.png) + +

(back to top)

+ +## Application Structure + +### Client + +#### MVC Model + +![MVC Diagram](.github/assets/mvc.png) + +The classes `Client` and `ClientUI` were left untouched, since there was no refactoring needed. +However, a major structural flaw of the client was, that the MVC pattern was not implemented. The +existing classes were tightly coupled together in many places, violating the principle of separation +of concerns. For example, parts of the view, model, and controller were mixed together in the +`ChatWindowController` class. To implement the MVC pattern, the code was split into the +following classes: + +##### Controller + +The `ChatWindowController` class should contain all the controls. Since there is only one controller +in the application, ChatWindowController is the only class that falls under this scope. The +following features were implemented in it: + +- JavaFX properties were used to allow the model classes to access the FXML fields saved in the + ChatWindowController. These properties were bound in the ChatWindowController using the Observer + pattern. +- All methods that didn't belong to any of the FXML controls were moved to either model or + view classes, adhering to the Single Responsibility Principle. + +#### Model + +The Model should contain the logic that executes changes in the software. The following classes were +created therefore: + +##### `ChatWindowModel` + +This class contains the core logic of the client, responsible for sending and receiving messages as +well as connecting and disconnecting from the server. It also stores most of the bindings and lets +the other classes access them through public getter methods. With that, other classes stay updated +about changes without giving them too much access. + +##### `MessageListModel` + +This class originated from the ClientMessageList class. It extends +`javafx.collections.ObservableListBase<>`, which provides built-in support for the Observer pattern, +allowing other classes to track changes in the list without explicit listening to it. It contains +methods to modify the message list. + +###### `FilteredMessageListModel` + +This class is similar to the MessageListModel class. It handles the features needed to filter +messages. Unlike in the original class, the filter was separated into a different class. +With the help of an ObjectProperty, the filter is adjustable in various classes. As well as in the +ChatWindowModel, the property is available through a public getter method. This class mainly +contains methods to adjust the filter. + +##### `MessageListView` + +The View only contains code to display updated information on the screen. It was implemented in the +MessageListView class. The MessageListView class gets the updated information through a binding with +the FilteredMessageListModel. With that, the view was updated when the FilteredMessageList, +respectively the MessageListModel, was updated. The only purpose of this class is to display the +information. + +#### Reasoning + +The reason for this refactor is on one hand that the implementation of the MVC model is considered +best practice and also a requirement for this project. On the other hand, we could improve the code +by splitting it up into sections. It got easier to understand how single classes work, and we could +adhere to fundamental principles of software design, such as the principle of separation of +concerns, the Single Responsibility Principle, and the Observer pattern. + +### Protocol + +#### Connection + +![Connection Diagram](.github/assets/connection.png) + +The protocol-level `Connection` class is a fundamental part of the application architecture. It +contains serializable connection information such as username, status and network connection, which +can be easily accessed with its getter and setter methods. In addition, the class contains the +sendPayload and closeConnection methods, which allow generic data packets to be sent over the +network, reacting appropriately to problems during transmission, and connections to be closed by +either the client or the server. + +The `ConnectionStateType` enum at the protocol level ensures that both the client and the server use +the same set of connection states, avoiding potential confusion and errors that could arise from +using different connection states. + +In addition, the abstract `ConnectionListener` class has been established at the protocol level to +handle incoming user data requests, handle method execution and error handling. The class is +implemented as runnable and is started as a thread on both the client and the server, keeping the +application stable and reliable. + +#### Payload + +![Payload Diagram](.github/assets/payload.png) + +The application has initially sent data via a string. This approach is not ideal for several +reasons. First, sending data as a string makes it difficult to standardise the format of the +exchanged data, which can lead to errors and data corruption. Second, sending data as a string can +be inefficient, especially when sending large amounts of data. + +By introducing a payload record that contains the type, sender, receiver and message, the format of +the exchanged data is standardised and the possibility of errors or data corruption is reduced. The +use of a PayloadFactory to create payload data and a PayloadHandler to handle payload data also +creates a more efficient and scalable way to handle communication between client and server. + +The `PayloadFactory` class provides a standardised way to create payloads for the various message +types, including Connect, Confirm, Disconnect and Message Types. By implementing +the `PayloadFactory`at the protocol level and using it on both the client and server side, it +ensures that payloads are created in a consistent and standardised manner throughout the +application. + +Using an enum set to specify the available payload types simplifies implementation and reduces +redundancy in the code. It allows the different payload types to be specified in one place, making +it easier to maintain and update the application in the future. + +The `PayloadHandler` interface provides a generic and extensible way to handle the different types +of payloads on the client and server side. By implementing the `PayloadHandler` for each payload +type, the payloads can be handled in a consistent manner, reducing errors and improving reliability. + +### Server + +#### Connection Registry + +![Connection Registry Diagram](.github/assets/connection-registry.png) + +The `ConnectionRegistry` class on the server side manages all active connections. It performs +various functions such as checking the availability of a username, ensuring that it matches the +username format and generating an available anonymous username. By standardising the format of the +username and ensuring that it is unique, the application can avoid potential errors that could arise +from duplicate usernames or incorrect formats. + +In addition to managing usernames, the ConnectionRegistry class also provides a mechanism for +registering and unregistering connections in the registry. This allows the application to keep track +of which clients are currently active and connected, which enables efficient processing of incoming +messages and ensures that the appropriate clients receive the relevant messages. + +

(back to top)

+ +## Issue List + +### Bugs + +All bugs have been assigned with the label fix + +1. Server connection of several clients not possible +2. Allocated anonymous usernames are not released on disconnect +3. Connected clients should disconnect on server shutdown +4. Server exception on non-port specified connection +5. Client connection thread not terminated on already taken username + +### Structural Issues + +All structural issues have been assigned with the label refactor + +1. Refactor and unify `proccesData()` method +2. Implementation of MVC model +3. Introduce connection registry class +4. Extract client and server `ConnectoinHanlder` into abstract class +5. Ensure proper exception handling and logging + +

(back to top)

+ +## GitHub Workflow + +### Code of Conduct + +Our [CODE OF CONDUCT](CODE_OF_CONDUCT.md) described the rules and guidelines for contributing to +our +project. + +### Codeowners + +We used the CODEOWNERS file to define the code owners for the repository. + +### Issues / Pull Requests + +Project management was done using GitHub Projects + +We used the following templates for issues and pull requests: + +- Issue labels +- [Issue template](.github%2FISSUE_TEMPLATE) +- [Pull request template](.github%2Fpull_request_template.md) + +### Branching Model + +We used the feature branching workflow for several reasons: + +- Firstly, it promotes better organization and management of code changes, especially those + involving multiple team members working on different features or tasks simultaneously. By creating + separate branches for each feature or task, developers can work on their code changes + independently without interfering with the work of others. This reduces the likelihood of + conflicts arising between different changes, which can be time-consuming to resolve. + +- Secondly, the feature branching workflow also enables better tracking of code changes and easier + identification of issues or bugs. Since each feature branch contains changes related to a specific + feature or task, it is easier to pinpoint issues and resolve them quickly. + +- Finally, the feature branching workflow also facilitates better quality control and helps ensure + that the project's overall codebase remains stable and functional. Changes are tested and reviewed + before they are merged back into the main branch, reducing the risk of introducing bugs or errors + into the production code. + +

(back to top)

diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a335675 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +# Reporting Security Issues + +If you believe you have found a security vulnerability in the codebase, we encourage you to let us +know right away. + +We will investigate all legitimate reports and do our best to quickly fix the problem. diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..4fa76d0 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,7 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + gradlePluginPortal() +} diff --git a/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.application-conventions.gradle b/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.application-conventions.gradle new file mode 100644 index 0000000..0884b73 --- /dev/null +++ b/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.application-conventions.gradle @@ -0,0 +1,15 @@ +plugins { + id 'ch.zhaw.pm2.multichat.common-conventions' + + id 'application' +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' +} + +test { + useJUnitPlatform() +} diff --git a/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.common-conventions.gradle b/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.common-conventions.gradle new file mode 100644 index 0000000..08c255c --- /dev/null +++ b/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.common-conventions.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.slf4j:slf4j-api:2.0.7" + implementation 'ch.qos.logback:logback-classic:1.4.6' + + compileOnly 'org.projectlombok:lombok:1.18.26' + annotationProcessor 'org.projectlombok:lombok:1.18.26' + + testCompileOnly 'org.projectlombok:lombok:1.18.26' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.26' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + + compileJava { + options.encoding = 'UTF-8' + } +} diff --git a/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.library-conventions.gradle b/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.library-conventions.gradle new file mode 100644 index 0000000..d23b5f5 --- /dev/null +++ b/buildSrc/src/main/groovy/ch.zhaw.pm2.multichat.library-conventions.gradle @@ -0,0 +1,5 @@ +plugins { + id 'ch.zhaw.pm2.multichat.common-conventions' + + id 'java-library' +} diff --git a/client/build.gradle b/client/build.gradle new file mode 100644 index 0000000..ac4d713 --- /dev/null +++ b/client/build.gradle @@ -0,0 +1,31 @@ +/* + * Gradle build configuration for specific lab module / exercise + */ +// enabled plugins +plugins { + // Support for Java applications + id 'ch.zhaw.pm2.multichat.application-conventions' + // Support for JavaFX + id 'org.openjfx.javafxplugin' version '0.0.13' +} + +// Project/Module information +description = 'Uebung Multichat – Client Application' +group = 'ch.zhaw.pm2' +version = '2023' + +dependencies { + // dependency to the protocol library + implementation project(':protocol') +} + +// Configuration for Application plugin +application { + // Define the main class for the application. + mainClass = 'ch.zhaw.pm2.multichat.client.Client' +} + +javafx { + version = '17.0.6' + modules = ['javafx.controls', 'javafx.fxml'] +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/Client.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/Client.java new file mode 100644 index 0000000..b5b7bd7 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/Client.java @@ -0,0 +1,24 @@ +package ch.zhaw.pm2.multichat.client; + +import javafx.application.Application; +import lombok.extern.slf4j.Slf4j; + +/** + * Entry point for the MultiChat client application. This class contains a `main` method that + * launches the JavaFX-based client user interface (`ClientUI`). It serves as the starting point for + * the client application. + */ +@Slf4j +public class Client { + + /** + * Launches the MultiChat client application. This method starts the JavaFX application by + * calling `Application.launch` and passing it the `ClientUI` class and command line arguments. + * + * @param args Command line arguments passed to the application. + */ + public static void main(String[] args) { + Application.launch(ClientUI.class, args); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/ClientUI.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/ClientUI.java new file mode 100644 index 0000000..5ff5f69 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/ClientUI.java @@ -0,0 +1,58 @@ +package ch.zhaw.pm2.multichat.client; + +import static java.lang.String.format; + +import ch.zhaw.pm2.multichat.client.controller.ChatWindowController; +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.stage.Stage; +import lombok.extern.slf4j.Slf4j; + +/** + * The {@code ClientUI} class represents the main entry point for the Multichat Client application. + * It extends the JavaFX {@link Application} class and provides a method for starting up the chat + * window. + */ +@Slf4j +public class ClientUI extends Application { + + /** + * {@inheritDoc} + */ + @Override + public void start(Stage primaryStage) { + chatWindow(primaryStage); + } + + /** + * Initializes the chat window and shows it on the primary stage. This method loads the + * ChatWindow.fxml file using {@link FXMLLoader}, sets up the controller, creates a new + * {@link Scene} from the loaded pane, and finally shows the primary stage. + * + * @param primaryStage the primary stage for this application + */ + private void chatWindow(Stage primaryStage) { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("ChatWindow.fxml")); + Pane rootPane = loader.load(); + + ChatWindowController controller = loader.getController(); + controller.initializeWithStage(primaryStage); + + // fill in scene and stage setup + Scene scene = new Scene(rootPane); + + // configure and show stage + primaryStage.setScene(scene); + primaryStage.setMinWidth(450); + primaryStage.setMinHeight(250); + primaryStage.setTitle("Multichat Client"); + primaryStage.show(); + } catch (Exception e) { + log.error(format("Error starting up the UI: %s", e.getMessage())); + } + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/connection/ClientConnectionListener.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/connection/ClientConnectionListener.java new file mode 100644 index 0000000..ad571b5 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/connection/ClientConnectionListener.java @@ -0,0 +1,78 @@ +package ch.zhaw.pm2.multichat.client.connection; + +import static ch.zhaw.pm2.multichat.client.message.MessageFactory.createErrorMessage; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONNECTED; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadFactory.createErrorPayload; +import static java.util.Objects.requireNonNull; +import static javafx.application.Platform.runLater; + +import ch.zhaw.pm2.multichat.client.model.ChatWindowModel; +import ch.zhaw.pm2.multichat.protocol.connection.Connection; +import ch.zhaw.pm2.multichat.protocol.connection.ConnectionListener; +import ch.zhaw.pm2.multichat.protocol.payload.Payload; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadHandler; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadType; +import java.util.Map; + +/** + * A class representing a listener for a client-side connection to a chat server. It extends the + * ConnectionListener class, which is a generic class that handles incoming and outgoing payloads of + * type Payload. + */ +public class ClientConnectionListener extends ConnectionListener { + + private final ChatWindowModel model; + + /** + * Constructor for the ClientConnectionListener class. + * + * @param connection the connection object to listen to + * @param payloadHandlers a map of payload types to their corresponding payload handlers + * @param model the chat window model + */ + public ClientConnectionListener(final Connection connection, + final Map> payloadHandlers, + final ChatWindowModel model) { + super(connection, payloadHandlers); + this.model = requireNonNull(model); + } + + /** + * Returns the PayloadType of a given Payload object. + * + * @param payload The Payload object to get the type of. + * @return The PayloadType of the given Payload object. + */ + @Override + protected PayloadType getPayloadType(Payload payload) { + return payload.type(); + } + + /** + * Creates an ErrorPayload object for a given error message. + * + * @param message The error message to create the ErrorPayload for. + * @return The ErrorPayload object for the given error message. + */ + @Override + protected Payload performErrorPayloadCreation(String message) { + return createErrorPayload(getConnection().getUsername(), message); + } + + /** + * Performs the disconnection sequence when a connection is lost or closed. This includes + * clearing the messages in the ChatWindowModel, adding a disconnection error message, and + * disconnecting the model from the connection. + */ + @Override + protected void performDisconnection() { + runLater(() -> { + if (getConnection().isState(CONNECTED)) { + model.clearMessages(); + model.addMessage(createErrorMessage("Disconnected due to connection error")); + } + model.disconnect(); + }); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/controller/ChatWindowController.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/controller/ChatWindowController.java new file mode 100644 index 0000000..c03688c --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/controller/ChatWindowController.java @@ -0,0 +1,233 @@ +package ch.zhaw.pm2.multichat.client.controller; + +import static ch.zhaw.pm2.multichat.client.message.MessageCategory.LOG; +import static ch.zhaw.pm2.multichat.client.message.MessageCategory.MESSAGE; +import static ch.zhaw.pm2.multichat.client.message.MessageFactory.createErrorMessage; +import static ch.zhaw.pm2.multichat.protocol.Identifiers.ANONYMOUS; +import static java.lang.String.format; + +import ch.zhaw.pm2.multichat.client.message.MessageCategory; +import ch.zhaw.pm2.multichat.client.message.MessageFilter; +import ch.zhaw.pm2.multichat.client.model.ChatWindowModel; +import ch.zhaw.pm2.multichat.client.utils.Formatter; +import ch.zhaw.pm2.multichat.client.utils.MessageParser; +import ch.zhaw.pm2.multichat.client.utils.MessageParser.MessageParserResult; +import ch.zhaw.pm2.multichat.client.view.MessageListView; +import ch.zhaw.pm2.multichat.protocol.NetworkHandler; +import ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType; +import ch.zhaw.pm2.multichat.protocol.exception.ChatProtocolException; +import java.net.URL; +import java.util.HashSet; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.regex.Pattern; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.MenuButton; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; +import lombok.extern.slf4j.Slf4j; + +/** + * A class representing the controller for the chat application. + */ +@Slf4j +public class ChatWindowController implements Initializable { + + private static final Pattern PORT_PATTERN = Pattern.compile("\\d{0,5}"); + private static final Pattern ADRESS_PATTERN = Pattern.compile("(\\d{0,3}(\\.\\d{0,3}){0,3})"); + private static final Pattern HOSTNAME_PATTERN = Pattern.compile( + "[A-Za-z\\-]+(\\.[A-Za-z\\-]+)*"); + private static final Pattern USERNAME_PATTERN = Pattern.compile("\\S{0,50}"); + + private BooleanBinding isServerAddressInvalidBinding; + private ChatWindowModel model; + + @FXML + private MenuButton filterOptionMenu; + + @FXML + private CheckMenuItem filterMessages; + + @FXML + private CheckMenuItem filterLogs; + + @FXML + private BorderPane rootPane; + + @FXML + private TextField serverAddressField; + + @FXML + private TextField serverPortField; + + @FXML + private TextField userNameField; + + @FXML + private TextField messageField; + + @FXML + private MessageListView messageArea; + + @FXML + private Button connectButton; + + @FXML + private Button sendButton; + + @FXML + private Button clearButton; + + @FXML + private TextField filterValue; + + /** + * Initializes the chat window by setting up the UI components, disabling and enabling various + * elements based on their respective bindings, and setting up listeners for input changes. + * + * @param location the URL of the fxml file that defines the layout of the chat window + * @param resources the ResourceBundle that contains the locale-specific resources for the chat + * window + */ + @FXML + public void initialize(URL location, ResourceBundle resources) { + model = new ChatWindowModel(); + + isServerAddressInvalidBinding = Bindings.or(model.isWaitingForResponseBinding(), + Bindings.or(serverAddressField.textProperty().isEmpty(), + serverPortField.textProperty().isEmpty())); + + // Initialize MessageArea + messageArea = new MessageListView(model.getFilteredMessages()); + messageArea.setId("messageArea"); + messageArea.setFocusTraversable(false); + messageArea.setPadding(new Insets(0, 5, 0, 5)); + rootPane.setCenter(messageArea); + + // Disable elements based on binding state + serverAddressField.disableProperty().bind(model.isConnectionEstablishedBinding()); + serverPortField.disableProperty().bind(model.isConnectionEstablishedBinding()); + userNameField.disableProperty().bind(model.isConnectionEstablishedBinding()); + connectButton.setDisable(true); + connectButton.disableProperty().bind(isServerAddressInvalidBinding); + sendButton.disableProperty().bind(model.isConnectionEstablishedBinding().not() + .or(Bindings.createBooleanBinding( + () -> MessageParser.parse(messageField.getText()).message().isBlank(), + messageField.textProperty()))); + messageField.disableProperty().bind(model.isConnectionEstablishedBinding().not()); + clearButton.disableProperty().bind(model.isMessageListEmptyBinding()); + filterValue.disableProperty().bind(model.isMessageListEmptyBinding()); + filterOptionMenu.disableProperty().bind(model.isMessageListEmptyBinding()); + + // Set default input field values + serverAddressField.setText(NetworkHandler.DEFAULT_ADDRESS.getCanonicalHostName()); + serverPortField.setText(String.valueOf(NetworkHandler.DEFAULT_PORT)); + + // Set input field formatter + userNameField.setTextFormatter(Formatter.format(USERNAME_PATTERN)); + serverAddressField.setTextFormatter(Formatter.format(HOSTNAME_PATTERN, ADRESS_PATTERN)); + serverPortField.setTextFormatter(Formatter.format(PORT_PATTERN)); + + // Bind username to property + userNameField.textProperty().bindBidirectional(model.getUsernameProperty()); + + // Set onChange listeners + filterValue.textProperty().addListener(observable -> onFilterInputChange()); + filterLogs.selectedProperty().addListener(observable -> onFilterInputChange()); + filterMessages.selectedProperty().addListener(observable -> onFilterInputChange()); + model.getStateProperty() + .addListener((observable, oldValue, newValue) -> onConnectButtonTextChange(newValue)); + } + + /** + * Initializes the window's stage by setting an event listener to handle the stage's close + * request and disconnecting the model from the network when the stage is closed. + * + * @param stage The JavaFX stage to initialize with the event listener. + */ + public void initializeWithStage(Stage stage) { + stage.setOnCloseRequest(e -> model.disconnect()); + } + + @FXML + private void onConnectButtonClick() { + if (model.isConnectionEstablished()) { + disconnect(); + messageField.clear(); + } else { + if (userNameField.getText() == null || userNameField.getText().isBlank()) { + userNameField.setText(ANONYMOUS); + } + + connect(); + } + } + + @FXML + private void onSendButtonClick() { + try { + MessageParserResult result = MessageParser.parse(messageField.getText().strip()); + if (!result.message().isBlank()) { + model.send(result.receiver(), result.message()); + messageField.clear(); + messageField.requestFocus(); + } + } catch (ChatProtocolException e) { + model.addMessage(createErrorMessage(e.getMessage())); + } + } + + @FXML + private void onClearButtonClick() { + model.clearMessages(); + messageField.requestFocus(); + } + + private void onFilterInputChange() { + Set categories = new HashSet<>(); + if (filterLogs.isSelected()) { + categories.add(LOG); + } + if (filterMessages.isSelected()) { + categories.add(MESSAGE); + } + + model.setFilter(new MessageFilter(categories, filterValue.getText())); + } + + private void onConnectButtonTextChange(ConnectionStateType value) { + connectButton.setText(switch (value) { + case NEW, CONFIRM_CONNECT -> "Connecting..."; + case CONNECTED -> "Disconnect"; + case CONFIRM_DISCONNECT -> "Disconnecting..."; + case DISCONNECTED -> "Connect"; + }); + } + + private void connect() { + if (isServerAddressInvalidBinding.get()) { + return; + } + + try { + model.connect(serverAddressField.getText(), + Integer.parseInt(serverPortField.getText())); + messageField.requestFocus(); + } catch (NumberFormatException e) { + model.addMessage(createErrorMessage(format("Invalid port number: %s", e.getMessage()))); + } + } + + private void disconnect() { + model.disconnect(); + } + +} + diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/message/Message.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/Message.java new file mode 100644 index 0000000..8da6f1d --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/Message.java @@ -0,0 +1,47 @@ +package ch.zhaw.pm2.multichat.client.message; + +import static java.util.Objects.requireNonNull; + +/** + * A class representing a message sent between clients in the MultiChat application. A message + * consists of a type, sender, receiver, and content. + */ +public record Message(MessageType type, String sender, String receiver, String content) { + + /** + * Constructs a new Message object with the given message type, sender, receiver, and content. + * + * @param type the type of the message + * @param sender the sender of the message + * @param receiver the receiver of the message + * @param content the content of the message + */ + public Message(MessageType type, String sender, String receiver, String content) { + this.type = requireNonNull(type); + this.sender = requireNonNull(sender); + this.receiver = requireNonNull(receiver); + this.content = requireNonNull(content); + } + + /** + * Returns the formatted message for this object. + * + * @return the formatted message as a String + */ + public String getMessage() { + return type.getFormattedMessage(this); + } + + /** + * Determines if the message satisfies the given message filter. + * + * @param filter the message filter to apply + * @return true if the message satisfies the filter, false otherwise + */ + public boolean isFilterApplicable(MessageFilter filter) { + return filter.categories().contains(type.getCategory()) && getMessage().toLowerCase() + .contains( + filter.content().toLowerCase()); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageCategory.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageCategory.java new file mode 100644 index 0000000..8164196 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageCategory.java @@ -0,0 +1,16 @@ +package ch.zhaw.pm2.multichat.client.message; + +/** + * An enum class with categories for a message. + */ +public enum MessageCategory { + /** + * Represents a regular message. + */ + MESSAGE, + + /** + * Represents a log message. + */ + LOG +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageFactory.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageFactory.java new file mode 100644 index 0000000..2207b91 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageFactory.java @@ -0,0 +1,51 @@ +package ch.zhaw.pm2.multichat.client.message; + +import static ch.zhaw.pm2.multichat.client.message.MessageType.CONVERSATION; +import static ch.zhaw.pm2.multichat.client.message.MessageType.ERROR; +import static ch.zhaw.pm2.multichat.client.message.MessageType.INFO; +import static ch.zhaw.pm2.multichat.protocol.Identifiers.SYSTEM; + +/** + * A utility class representing a factory for {@link Message} objects. + */ +public class MessageFactory { + + + private MessageFactory() { + // private constructor to prevent instantiation + } + + /** + * Creates a new {@link Message} object of type {@link MessageType#CONVERSATION}. + * + * @param sender the sender of the message + * @param receiver the receiver of the message + * @param message the content of the message + * @return a new {@link Message} object + */ + public static Message createConversationMessage(String sender, String receiver, + String message) { + return new Message(CONVERSATION, sender, receiver, message); + } + + /** + * Creates a new {@link Message} object of type {@link MessageType#INFO}. + * + * @param message the content of the message + * @return a new {@link Message} object + */ + public static Message createInfoMessage(String message) { + return new Message(INFO, SYSTEM, SYSTEM, message); + } + + /** + * Creates a new {@link Message} object of type {@link MessageType#ERROR}. + * + * @param message the content of the message + * @return a new {@link Message} object + */ + public static Message createErrorMessage(String message) { + return new Message(ERROR, SYSTEM, SYSTEM, message); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageFilter.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageFilter.java new file mode 100644 index 0000000..5350c26 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageFilter.java @@ -0,0 +1,17 @@ +package ch.zhaw.pm2.multichat.client.message; + +import java.util.Set; + +/** + * A class representing a filter for messages that can be applied to a chat window. + */ +public record MessageFilter(Set categories, String content) { + + /** + * A message filter that includes all categories and matches any message. + */ + public static final MessageFilter ALL_FILTER_OPTIONS = new MessageFilter( + Set.of(MessageCategory.values()), + ""); + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageType.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageType.java new file mode 100644 index 0000000..b326542 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/message/MessageType.java @@ -0,0 +1,70 @@ +package ch.zhaw.pm2.multichat.client.message; + +import static ch.zhaw.pm2.multichat.client.message.MessageCategory.LOG; +import static ch.zhaw.pm2.multichat.client.message.MessageCategory.MESSAGE; + +/** + * An enum class representing the type of messages that can be sent by the client. Each message type + * has a corresponding {@link MessageCategory} and a method to format the message. + */ +public enum MessageType { + /** + * Represents a conversation message. + */ + CONVERSATION(MESSAGE) { + @Override + String getFormattedMessage(Message message) { + return String.format("[%s -> %s] %s", message.sender(), message.receiver(), + message.content()); + } + }, + + /** + * Represents an information log message. + */ + INFO(LOG) { + @Override + String getFormattedMessage(Message message) { + return String.format("[INFO] %s", message.content()); + } + }, + + /** + * Represents an error log message. + */ + ERROR(LOG) { + @Override + String getFormattedMessage(Message message) { + return String.format("[ERROR] %s", message.content()); + } + }; + + private final MessageCategory category; + + /** + * Enum constructor for the different message types in the system. + * + * @param category the message category of the message type + */ + MessageType(MessageCategory category) { + this.category = category; + } + + /** + * Returns the category of this message type. + * + * @return the category of the message type + */ + public MessageCategory getCategory() { + return category; + } + + /** + * Returns the formatted message for the given message. + * + * @param message the message to format + * @return the formatted message as a String + */ + abstract String getFormattedMessage(Message message); + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/model/ChatWindowModel.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/model/ChatWindowModel.java new file mode 100644 index 0000000..5f192c6 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/model/ChatWindowModel.java @@ -0,0 +1,322 @@ +package ch.zhaw.pm2.multichat.client.model; + +import static ch.zhaw.pm2.multichat.client.message.MessageFactory.createErrorMessage; +import static ch.zhaw.pm2.multichat.client.message.MessageFilter.ALL_FILTER_OPTIONS; +import static ch.zhaw.pm2.multichat.protocol.NetworkHandler.openConnection; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONFIRM_CONNECT; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONFIRM_DISCONNECT; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONNECTED; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.DISCONNECTED; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.NEW; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadFactory.createConnectPayload; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadFactory.createDisconnectPayload; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadFactory.createMessagePayload; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadType.CONFIRM; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadType.CONNECT; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadType.DISCONNECT; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadType.ERROR; +import static ch.zhaw.pm2.multichat.protocol.payload.PayloadType.MESSAGE; +import static java.lang.String.format; +import static javafx.collections.FXCollections.unmodifiableObservableList; + +import ch.zhaw.pm2.multichat.client.connection.ClientConnectionListener; +import ch.zhaw.pm2.multichat.client.message.Message; +import ch.zhaw.pm2.multichat.client.message.MessageFilter; +import ch.zhaw.pm2.multichat.client.payload.ConfirmPayloadHandler; +import ch.zhaw.pm2.multichat.client.payload.ConnectPayloadHandler; +import ch.zhaw.pm2.multichat.client.payload.DisconnectPayloadHandler; +import ch.zhaw.pm2.multichat.client.payload.ErrorPayloadHandler; +import ch.zhaw.pm2.multichat.client.payload.MessagePayloadHandler; +import ch.zhaw.pm2.multichat.protocol.connection.Connection; +import ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType; +import ch.zhaw.pm2.multichat.protocol.exception.ChatProtocolException; +import ch.zhaw.pm2.multichat.protocol.payload.Payload; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadHandler; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadType; +import java.io.IOException; +import java.util.EnumMap; +import java.util.Map; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; +import lombok.extern.slf4j.Slf4j; + +/** + * A class representing the model for the chat window controller. It manages the connection and + * messaging functionalities with the chat server. It provides the functionality to establish, + * disconnect, and manage the state of the connection, retrieve and filter the messages, add a + * message, and clear the message list. It also provides the binding and properties for the + * username, connection state, message list, and waiting for the response status. + */ +@Slf4j +public class ChatWindowModel { + + private final MessageListModel messages = new MessageListModel(); + private final StringProperty usernameProperty = new SimpleStringProperty(); + private final Map> payloadHandlers = new EnumMap<>( + PayloadType.class); + private final ObjectProperty stateProperty = new SimpleObjectProperty<>( + DISCONNECTED); + private final BooleanBinding isWaitingForResponseBinding = Bindings.or( + stateProperty.isEqualTo(CONFIRM_CONNECT), stateProperty.isEqualTo(CONFIRM_DISCONNECT)); + private final BooleanBinding isConnectionEstablishedBinding = stateProperty.isEqualTo( + CONNECTED); + private final BooleanBinding isMessageListEmptyBinding = messages.getSizeBinding().isEqualTo( + 0); + private final FilteredMessageListModel filteredMessages; + private Connection connection; + + /** + * Constructs a new ChatWindowModel object. Initializes the filteredMessages attribute as a new + * instance of FilteredMessageListModel, passing in the messages attribute. + */ + public ChatWindowModel() { + filteredMessages = new FilteredMessageListModel(messages); + initializePayloadHandlers(); + } + + /** + * Returns a StringProperty containing the username. + * + * @return usernameProperty. + */ + public StringProperty getUsernameProperty() { + return usernameProperty; + } + + /** + * Returns the username. + * + * @return username of connection. + */ + public String getUsername() { + return usernameProperty.get(); + } + + /** + * Method to set the username. + * + * @param username username for client connection. + */ + public void setUsername(String username) { + usernameProperty.set(username); + } + + /** + * Method to clear the username. + */ + public void clearUsername() { + usernameProperty.set(""); + } + + /** + * Returns stateProperty. + * + * @return ObjectProperty + */ + public ObjectProperty getStateProperty() { + return stateProperty; + } + + /** + * Method to set the state of a connection. + * + * @param state ConnectionStateType of the connection + */ + public void setState(ConnectionStateType state) { + stateProperty.set(state); + + if (connection != null) { + connection.setState(state); + } + } + + /** + * Checks if the current state of the connection is equal to the specified state. + * + * @param state the ConnectionStateType to compare with the current state + * @return true if the current state is equal to the specified state, false otherwise + */ + public boolean isState(ConnectionStateType state) { + return stateProperty.get() == state; + } + + /** + * Returns a BooleanBinding that represents whether the chat window model is currently waiting + * for a response from the server. + * + * @return A BooleanBinding that represents whether the chat window model is currently waiting + * for a response from the server. + */ + public BooleanBinding isWaitingForResponseBinding() { + return isWaitingForResponseBinding; + } + + /** + * Returns a BooleanBinding that represents whether the connection to the server has been + * established or not. + * + * @return A BooleanBinding that is true if the connection to the server has been established, + * false otherwise. + */ + public BooleanBinding isConnectionEstablishedBinding() { + return isConnectionEstablishedBinding; + } + + /** + * Returns whether the connection with the server has been established. + * + * @return {@code true} if the connection has been established, {@code false} otherwise. + */ + public boolean isConnectionEstablished() { + return isConnectionEstablishedBinding.get(); + } + + /** + * Returns a BooleanBinding representing whether the message list is empty or not. + * + * @return a BooleanBinding representing whether the message list is empty or not. + */ + public BooleanBinding isMessageListEmptyBinding() { + return isMessageListEmptyBinding; + } + + /** + * Sets the message filter to the specified filter and updates the filtered messages list + * accordingly. + * + * @param filter the message filter to set. + */ + public void setFilter(MessageFilter filter) { + filteredMessages.setFilter(filter); + } + + /** + * Returns an ObservableList with the filtered messages. + * + * @return ObservableList containing filtered messages. + */ + public ObservableList getFilteredMessages() { + return unmodifiableObservableList(filteredMessages); + } + + /** + * Adds a new message to the list. + * + * @param message message that is added. + */ + public void addMessage(Message message) { + messages.add(message); + } + + /** + * Deletes all messages. + */ + public void clearMessages() { + messages.clear(); + } + + /** + * Connects to a chat server at the specified host and port. If the connection is already + * established, an error message is added to the chat window and the method returns. Otherwise, + * the connection is established and a new thread is started to listen for incoming payloads. A + * "connect" payload is sent to the server with the username of the client, and the client state + * is set to CONFIRM_CONNECT. If an I/O exception or chat protocol exception occurs, an error + * message is added to the chat window and the disconnect() method is called to clean up. + * + * @param host the host to connect to + * @param port the port to connect to + */ + public void connect(String host, int port) { + clearMessages(); + + if (!isState(DISCONNECTED)) { + addMessage(createErrorMessage("Connection is already established")); + return; + } + + setState(NEW); + setFilter(ALL_FILTER_OPTIONS); + + try { + connection = new Connection<>(openConnection(host, port)); + new Thread(new ClientConnectionListener(connection, payloadHandlers, this)).start(); + + if (!isState(NEW)) { + throw new ChatProtocolException( + format("Illegal state for connection: %s", connection.getState())); + } + + connection.sendPayload(createConnectPayload(getUsername())); + setState(CONFIRM_CONNECT); + } catch (IOException | ChatProtocolException e) { + addMessage(createErrorMessage(e.getMessage())); + disconnect(); + } + } + + /** + * Disconnects from the chat server. If the connection is not established, this method does + * nothing. Otherwise, a "disconnect" payload is sent to the server with the username of the + * client. If the network connection is not available, the client state is set to DISCONNECTED + * and the username is cleared. Otherwise, the messages are cleared and the client state is set + * to CONFIRM_DISCONNECT. Finally, the username is cleared. + */ + public void disconnect() { + if (connection != null && !connection.isState(DISCONNECTED)) { + connection.sendPayload(createDisconnectPayload(getUsername())); + + if (!connection.getNetworkConnection().isAvailable()) { + setState(DISCONNECTED); + clearUsername(); + + return; + } + + clearMessages(); + setState(CONFIRM_DISCONNECT); + } else { + setState(DISCONNECTED); + } + clearUsername(); + } + + /** + * Sends a message to the specified receiver through the established connection. If the + * connection is not established or the message is empty, it will add an error message to the + * chat. + * + * @param receiver the username of the message recipient + * @param content the content of the message + */ + public void send(String receiver, String content) { + if (connection == null || connection.getState() != CONNECTED) { + addMessage(createErrorMessage("Connection is not established")); + return; + } + + if (content == null || content.isBlank()) { + addMessage(createErrorMessage("Empty messages are not allowed to be sent")); + return; + } + + connection.sendPayload(createMessagePayload(getUsername(), receiver, content)); + } + + /** + * Initializes the payload handlers for each payload type. This method is called when the + * ChatWindowModel is created. + */ + private void initializePayloadHandlers() { + payloadHandlers.put(CONNECT, new ConnectPayloadHandler()); + payloadHandlers.put(CONFIRM, new ConfirmPayloadHandler(this)); + payloadHandlers.put(DISCONNECT, new DisconnectPayloadHandler(this)); + payloadHandlers.put(MESSAGE, new MessagePayloadHandler(this)); + payloadHandlers.put(ERROR, new ErrorPayloadHandler(this)); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/model/FilteredMessageListModel.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/model/FilteredMessageListModel.java new file mode 100644 index 0000000..032ace4 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/model/FilteredMessageListModel.java @@ -0,0 +1,126 @@ +package ch.zhaw.pm2.multichat.client.model; + +import static javafx.application.Platform.runLater; + +import ch.zhaw.pm2.multichat.client.message.Message; +import ch.zhaw.pm2.multichat.client.message.MessageFilter; +import ch.zhaw.pm2.multichat.client.utils.ConsumerTimeout; +import javafx.beans.Observable; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableListBase; +import javafx.collections.transformation.FilteredList; + +/** + * A class representing a filtered list of messages. It is a JavaFX ObservableList that contains + * messages and applies a filter to them. This class is used to display messages that match a + * certain criteria, such as a specific keyword or a certain sender. + */ +public class FilteredMessageListModel extends ObservableListBase { + + private final FilteredList messages; + private final ObjectProperty filterProperty = new SimpleObjectProperty<>(); + private final ConsumerTimeout consumerTimeout = new ConsumerTimeout<>( + this::refreshFilter, 200); + + /** + * Constructs a new FilteredMessageListModel object. + * + * @param messages The list of messages that the filter will be applied to. + */ + public FilteredMessageListModel(ObservableList messages) { + this.messages = new FilteredList<>(messages); + filterProperty.addListener(this::onFilterChange); + ListChangeListener listener = this::fireChange; + this.messages.addListener(listener); + } + + /** + * Sets the filter to be used to filter the messages. + * + * @param filter The MessageFilter to be used to filter the messages. + */ + public synchronized void setFilter(MessageFilter filter) { + filterProperty.set(filter); + } + + /** + * Returns the Message at the specified index in the list. + * + * @param index The index of the Message to be returned. + * @return The Message at the specified index in the list. + */ + @Override + public synchronized Message get(int index) { + return messages.get(index); + } + + /** + * Returns the number of messages in the filtered list. + * + * @return The number of messages in the filtered list. + */ + @Override + public synchronized int size() { + return messages.size(); + } + + /** + * Returns true if the specified object is equal to this FilteredMessageListModel object, false + * otherwise. + * + * @param o The object to compare to this FilteredMessageListModel object. + * @return True if the specified object is equal to this FilteredMessageListModel object, false + * otherwise. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FilteredMessageListModel that = (FilteredMessageListModel) o; + return messages.equals(that.messages); + } + + /** + * Returns the hash code of this FilteredMessageListModel object. + * + * @return The hash code of this FilteredMessageListModel object. + */ + @Override + public int hashCode() { + return messages.hashCode(); + } + + /** + * Called when the filter changes. Applies the new filter after a certain timeout. + * + * @param observable The Observable that triggered the event. + * @param oldFilter The old filter. + * @param newFilter The new filter. + */ + private void onFilterChange(Observable observable, MessageFilter oldFilter, + MessageFilter newFilter) { + consumerTimeout.accept(newFilter); + } + + /** + * Refreshes the current filter by setting the predicate of the underlying filtered list. The + * predicate is set to a lambda expression that determines whether a message is applicable to + * the specified filter or not. The operation is executed asynchronously on the JavaFX + * application thread using {@link javafx.application.Platform#runLater(Runnable)}. This is to + * ensure that the update to the list is done on the JavaFX thread, which is required for + * thread-safety reasons. + * + * @param filter the new filter to apply to the list + */ + private void refreshFilter(MessageFilter filter) { + runLater(() -> messages.setPredicate(m -> m.isFilterApplicable(filter))); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/model/MessageListModel.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/model/MessageListModel.java new file mode 100644 index 0000000..0f01c73 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/model/MessageListModel.java @@ -0,0 +1,110 @@ +package ch.zhaw.pm2.multichat.client.model; + +import static javafx.beans.binding.Bindings.createIntegerBinding; +import static javafx.collections.FXCollections.observableArrayList; + +import ch.zhaw.pm2.multichat.client.message.Message; +import javafx.beans.binding.IntegerBinding; +import javafx.collections.ObservableList; +import javafx.collections.ObservableListBase; + +/** + * A model class for a list of chat messages. This class extends {@link ObservableListBase}, + * providing the necessary functionality to create an observable list. The class also exposes an + * {@link IntegerBinding} that is bound to the size of the list, allowing clients to track the + * number of messages in the list without the need to listen for changes to the list directly. + */ +public class MessageListModel extends ObservableListBase { + + private final ObservableList messages; + private final IntegerBinding sizeBinding = createIntegerBinding(this::size, this); + + /** + * Creates a new instance of {@code MessageListModel} with an empty list of messages. The list + * is an observable list, so it can be observed for changes. + */ + public MessageListModel() { + messages = observableArrayList(); + messages.addListener(this::fireChange); + } + + /** + * Adds the given message at the specified index in the list. + * + * @param index the index at which to insert the message + * @param message the message to add to the list + */ + @Override + public synchronized void add(int index, Message message) { + messages.add(index, message); + } + + /** + * Returns the message at the specified index in the list. + * + * @param index the index of the message to retrieve + * @return the message at the specified index + */ + @Override + public synchronized Message get(int index) { + return messages.get(index); + } + + /** + * Removes all messages from the list. + */ + @Override + public synchronized void clear() { + messages.clear(); + } + + /** + * Returns the number of messages in the list. + * + * @return the number of messages in the list + */ + @Override + public synchronized int size() { + return messages.size(); + } + + /** + * Returns {@code true} if the given object is equal to this object, and {@code false} + * otherwise. Two {@code MessageListModel} instances are considered equal if their underlying + * message lists are equal. + * + * @param o the object to compare to this object + * @return {@code true} if the objects are equal, {@code false} otherwise + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MessageListModel that = (MessageListModel) o; + return messages.equals(that.messages); + } + + /** + * Returns the hash code of this object. + * + * @return the hash code of this object + */ + @Override + public int hashCode() { + return messages.hashCode(); + } + + /** + * Returns the {@link IntegerBinding} that is bound to the size of the list. + * + * @return the {@code IntegerBinding} that is bound to the size of the list + */ + public synchronized IntegerBinding getSizeBinding() { + return sizeBinding; + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ConfirmPayloadHandler.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ConfirmPayloadHandler.java new file mode 100644 index 0000000..d57b77c --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ConfirmPayloadHandler.java @@ -0,0 +1,61 @@ +package ch.zhaw.pm2.multichat.client.payload; + +import static ch.zhaw.pm2.multichat.client.message.MessageFactory.createInfoMessage; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONFIRM_CONNECT; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONFIRM_DISCONNECT; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONNECTED; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.DISCONNECTED; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static javafx.application.Platform.runLater; + +import ch.zhaw.pm2.multichat.client.model.ChatWindowModel; +import ch.zhaw.pm2.multichat.protocol.connection.Connection; +import ch.zhaw.pm2.multichat.protocol.payload.Payload; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * Class representing a handler that handles confirm payloads sent by the server. The confirm + * payload is used to confirm connection and disconnection requests. This handler updates the chat + * window model accordingly based on the state of the connection. + */ +@Slf4j +public class ConfirmPayloadHandler implements PayloadHandler { + + private final ChatWindowModel model; + + /** + * Constructs a new confirm-payload handler with the given chat window model. + * + * @param model the chat window model to update based on the payload + */ + public ConfirmPayloadHandler(ChatWindowModel model) { + this.model = requireNonNull(model); + } + + /** + * Handles the given confirm payload by updating the chat window model accordingly based on the + * state of the connection. + * + * @param payload the confirm-payload to handle + * @param connection the connection associated with the payload + */ + @Override + public void handle(Payload payload, Connection connection) { + runLater(() -> { + if (connection.isState(CONFIRM_CONNECT)) { + connection.setUsername(payload.receiver()); + model.setUsername(connection.getUsername()); + model.addMessage(createInfoMessage(payload.content())); + model.setState(CONNECTED); + } else if (connection.isState(CONFIRM_DISCONNECT)) { + model.addMessage(createInfoMessage(payload.content())); + model.setState(DISCONNECTED); + } else { + log.error(format("Got unexpected confirm content: %s", connection.getState())); + } + }); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ConnectPayloadHandler.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ConnectPayloadHandler.java new file mode 100644 index 0000000..ad54b88 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ConnectPayloadHandler.java @@ -0,0 +1,28 @@ +package ch.zhaw.pm2.multichat.client.payload; + +import static java.lang.String.format; + +import ch.zhaw.pm2.multichat.protocol.connection.Connection; +import ch.zhaw.pm2.multichat.protocol.payload.Payload; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadHandler; + +/** + * A class representing a PayloadHandler for handling ConnectPayloads received from the server. This + * class throws an UnsupportedOperationException since ConnectPayload is not supported client side. + */ +public class ConnectPayloadHandler implements PayloadHandler { + + /** + * Throws an UnsupportedOperationException since ConnectPayload is not supported client side. + * + * @param payload The payload received from the server + * @param connection The connection handling the payload + * @throws UnsupportedOperationException always + */ + @Override + public void handle(Payload payload, Connection connection) { + throw new UnsupportedOperationException( + format("%s is not supported client side", getClass().getCanonicalName())); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/DisconnectPayloadHandler.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/DisconnectPayloadHandler.java new file mode 100644 index 0000000..6cd7bc5 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/DisconnectPayloadHandler.java @@ -0,0 +1,51 @@ +package ch.zhaw.pm2.multichat.client.payload; + +import static ch.zhaw.pm2.multichat.client.message.MessageFactory.createInfoMessage; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.DISCONNECTED; +import static java.util.Objects.requireNonNull; +import static javafx.application.Platform.runLater; + +import ch.zhaw.pm2.multichat.client.model.ChatWindowModel; +import ch.zhaw.pm2.multichat.protocol.connection.Connection; +import ch.zhaw.pm2.multichat.protocol.payload.Payload; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * A class representing DisconnectPayload Handler that is responsible for handling payloads that + * instruct the client to disconnect from the server. When the handler receives a disconnect + * payload, it closes the connection, sets the client's state to disconnected, and adds an info + * message to the chat window model indicating that the user has disconnected. + */ +@Slf4j +public class DisconnectPayloadHandler implements PayloadHandler { + + private final ChatWindowModel model; + + public DisconnectPayloadHandler(ChatWindowModel model) { + this.model = requireNonNull(model); + } + + /** + * Handles a disconnect payload received from the server. Closes the connection and sets the + * client's state to disconnected. Adds an info message to the chat window model indicating that + * the user has disconnected. + * + * @param payload the disconnect payload to handle + * @param connection the connection to the server + */ + @Override + public void handle(Payload payload, Connection connection) { + runLater(() -> { + if (connection.isState(DISCONNECTED)) { + log.info("Connection is already disconnected"); + return; + } + + connection.closeConnection(); + model.setState(DISCONNECTED); + model.addMessage(createInfoMessage(payload.content())); + }); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ErrorPayloadHandler.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ErrorPayloadHandler.java new file mode 100644 index 0000000..35cf251 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/ErrorPayloadHandler.java @@ -0,0 +1,58 @@ +package ch.zhaw.pm2.multichat.client.payload; + +import static ch.zhaw.pm2.multichat.client.message.MessageFactory.createErrorMessage; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONFIRM_CONNECT; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.DISCONNECTED; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static javafx.application.Platform.runLater; + +import ch.zhaw.pm2.multichat.client.model.ChatWindowModel; +import ch.zhaw.pm2.multichat.protocol.connection.Connection; +import ch.zhaw.pm2.multichat.protocol.payload.Payload; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * A class representing a ErrorPayloadHandler is responsible for handling error messages received + * from the server. It implements the PayloadHandler interface and defines the handle method which + * is called by the client when a new error message is received from the server. + */ +@Slf4j +public class ErrorPayloadHandler implements PayloadHandler { + + private final ChatWindowModel model; + + /** + * Constructs a new ErrorPayloadHandler with the given ChatWindowModel. + * + * @param model The ChatWindowModel to use for handling the error message. + */ + public ErrorPayloadHandler(ChatWindowModel model) { + this.model = requireNonNull(model); + } + + /** + * This method is called by the client when a new error message is received from the server. It + * logs the error message, adds it to the ChatWindowModel, and updates the state of the model if + * necessary. + * + * @param payload The Payload object containing the error message received from the server. + * @param connection The Connection object representing the connection to the server. + */ + @Override + public void handle(Payload payload, Connection connection) { + if (connection.isState(CONFIRM_CONNECT)) { + Thread.currentThread().interrupt(); + } + + log.error(format("Received error from server: %s", payload.content())); + runLater(() -> { + model.addMessage(createErrorMessage(payload.content())); + if (connection.isState(CONFIRM_CONNECT)) { + model.setState(DISCONNECTED); + } + }); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/MessagePayloadHandler.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/MessagePayloadHandler.java new file mode 100644 index 0000000..3729c96 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/payload/MessagePayloadHandler.java @@ -0,0 +1,55 @@ +package ch.zhaw.pm2.multichat.client.payload; + +import static ch.zhaw.pm2.multichat.client.message.MessageFactory.createConversationMessage; +import static ch.zhaw.pm2.multichat.protocol.connection.ConnectionStateType.CONNECTED; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static javafx.application.Platform.runLater; + +import ch.zhaw.pm2.multichat.client.model.ChatWindowModel; +import ch.zhaw.pm2.multichat.protocol.connection.Connection; +import ch.zhaw.pm2.multichat.protocol.payload.Payload; +import ch.zhaw.pm2.multichat.protocol.payload.PayloadHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * A class representing a PayloadHandler implementation that handles incoming message payloads from + * the server. This handler adds incoming messages to the associated ChatWindowModel. + */ +@Slf4j +public class MessagePayloadHandler implements PayloadHandler { + + private final ChatWindowModel model; + + /** + * Constructs a new MessagePayloadHandler with the given ChatWindowModel. + * + * @param model The ChatWindowModel associated with this MessagePayloadHandler + */ + public MessagePayloadHandler(ChatWindowModel model) { + this.model = requireNonNull(model); + } + + /** + * Handles the incoming message payload by adding the message to the associated ChatWindowModel. + * If the connection is not in the CONNECTED state, logs an error and returns. + * + * @param payload The incoming message payload + * @param connection The connection associated with this payload + */ + @Override + public void handle(Payload payload, Connection connection) { + runLater(() -> { + if (!connection.isState(CONNECTED)) { + log.info( + format("Illegal state %s for content: %s", connection.getState(), + payload.content())); + return; + } + + model.addMessage( + createConversationMessage(payload.sender(), payload.receiver(), payload.content())); + }); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/ConsumerTimeout.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/ConsumerTimeout.java new file mode 100644 index 0000000..b4aa9bd --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/ConsumerTimeout.java @@ -0,0 +1,44 @@ +package ch.zhaw.pm2.multichat.client.utils; + +import java.util.function.Consumer; + +/** + * A class representing wrapper for a {@link Consumer} that imposes a minimum delay between + * invocations This ensures that the consumer is not called too frequently. + * + * @param the type of the input to the consumer + */ +public class ConsumerTimeout implements Consumer { + + private final Consumer consumer; + private final long timeout; + private long lastCall; + + /** + * Constructs a new {@code ConsumerTimeout} object. + * + * @param consumer the consumer to be wrapped + * @param timeout the minimum time between invocations, in milliseconds + */ + public ConsumerTimeout(Consumer consumer, long timeout) { + this.consumer = consumer; + this.timeout = timeout; + this.lastCall = System.currentTimeMillis(); + } + + /** + * Accepts the input argument, invoking the wrapped consumer only if the minimum time between + * invocations has elapsed since the last call. + * + * @param t the input argument + */ + @Override + public void accept(T t) { + long now = System.currentTimeMillis(); + if (now - lastCall > timeout) { + consumer.accept(t); + lastCall = now; + } + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/Formatter.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/Formatter.java new file mode 100644 index 0000000..6979610 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/Formatter.java @@ -0,0 +1,35 @@ +package ch.zhaw.pm2.multichat.client.utils; + +import java.util.regex.Pattern; +import javafx.scene.control.TextFormatter; + +/** + * A class representing a Formatter providing a utility method that enforces a set of regular + * expression patterns on input text. + */ +public class Formatter { + + private Formatter() { + // private constructor to prevent instantiation + } + + /** + * Returns a {@code TextFormatter} that only allows changes to the input text that match at + * least one of the specified regular expression patterns. + * + * @param patterns the regular expression patterns to match + * @return a {@code TextFormatter} that enforces the specified patterns + */ + public static TextFormatter format(Pattern... patterns) { + return new TextFormatter<>(change -> { + for (Pattern regex : patterns) { + if (regex.matcher(change.getControlNewText()).matches()) { + return change; + } + } + + return null; + }); + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/MessageParser.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/MessageParser.java new file mode 100644 index 0000000..a12bb43 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/utils/MessageParser.java @@ -0,0 +1,60 @@ +package ch.zhaw.pm2.multichat.client.utils; + +import static ch.zhaw.pm2.multichat.protocol.Identifiers.EVERYONE; +import static java.lang.String.format; + +import ch.zhaw.pm2.multichat.protocol.exception.ChatProtocolException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A class representing MessageParser used for parsing user input into message, receiver and + * content. + */ + +public class MessageParser { + + /** + * Regular expression pattern to parse user input into a content and recipient. + */ + private static final Pattern messagePattern = Pattern.compile("^(?:@(\\S*))?\\s*(.*)$"); + + /** + * Regular expression pattern to parse user input into a content and recipient. + */ + private MessageParser() { + // private constructor to prevent instantiation + } + + /** + * Parses the given message into a {@link MessageParserResult}, which contains the message + * receiver and content. + * + * @param message the message to parse + * @return the parsed {@link MessageParserResult} + * @throws ChatProtocolException if the message could not be parsed + */ + public static MessageParserResult parse(String message) throws ChatProtocolException { + Matcher matcher = messagePattern.matcher(message); + + if (matcher.find()) { + String receiver = matcher.group(1); + String content = matcher.group(2); + if (receiver == null || receiver.isBlank()) { + receiver = EVERYONE; + } + + return new MessageParserResult(receiver, content); + } else { + throw new ChatProtocolException(format("Couldn't parse message: %s", message)); + } + } + + /** + * A record to hold the message receiver and content after parsing. + */ + public record MessageParserResult(String receiver, String message) { + + } + +} diff --git a/client/src/main/java/ch/zhaw/pm2/multichat/client/view/MessageListView.java b/client/src/main/java/ch/zhaw/pm2/multichat/client/view/MessageListView.java new file mode 100644 index 0000000..b462b08 --- /dev/null +++ b/client/src/main/java/ch/zhaw/pm2/multichat/client/view/MessageListView.java @@ -0,0 +1,72 @@ +package ch.zhaw.pm2.multichat.client.view; + +import static java.util.Objects.requireNonNull; + +import ch.zhaw.pm2.multichat.client.message.Message; +import java.util.List; +import java.util.stream.Collectors; +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; +import javafx.scene.control.TextArea; +import javafx.scene.layout.BorderPane; + +/** + * A class representing a MessageListView used for displaying a list of messages in the JavaFX chat + * message application. + */ +public class MessageListView extends BorderPane { + + private final ObservableList messageList; + private TextArea textArea; + + /** + * Constructs a new instance of {@code MessageListView} with the specified + * {@link ObservableList} of messages. + * + * @param messageList the {@code ObservableList} of messages to be displayed in the view + * @throws NullPointerException if the {@code messageList} parameter is {@code null} + */ + public MessageListView(ObservableList messageList) { + this.messageList = requireNonNull(messageList); + initializeView(); + initializeMessageList(); + } + + private void initializeView() { + textArea = new TextArea(); + textArea.setEditable(false); + textArea.setWrapText(true); + setCenter(textArea); + textArea.focusTraversableProperty().bind(focusTraversableProperty()); + } + + private void initializeMessageList() { + messageList.addListener(this::refreshView); + textArea.clear(); + refreshMessageList(messageList); + } + + private void refreshView(Change change) { + boolean entireListRefreshed = false; + while (change.next() && !entireListRefreshed) { + if (change.wasUpdated() || change.wasRemoved() || change.wasReplaced() + || change.wasPermutated()) { + textArea.clear(); + entireListRefreshed = true; + } + + refreshMessageList(change.getAddedSubList()); + } + } + + private void refreshMessageList(List list) { + textArea.appendText( + list.stream().map(Message::getMessage) + .collect(Collectors.joining(System.lineSeparator()))); + + if (!list.isEmpty()) { + textArea.appendText(System.lineSeparator()); + } + } + +} diff --git a/client/src/main/resources/ch/zhaw/pm2/multichat/client/ChatWindow.fxml b/client/src/main/resources/ch/zhaw/pm2/multichat/client/ChatWindow.fxml new file mode 100644 index 0000000..b2d82ef --- /dev/null +++ b/client/src/main/resources/ch/zhaw/pm2/multichat/client/ChatWindow.fxml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + +