diff --git a/.github/actions/setup-jdk/action.yml b/.github/actions/setup-jdk/action.yml
new file mode 100644
index 0000000..209dfce
--- /dev/null
+++ b/.github/actions/setup-jdk/action.yml
@@ -0,0 +1,12 @@
+name: 'Setup JDK 25'
+description: 'Sets up Java 25 with Temurin and Maven cache'
+
+runs:
+ using: "composite"
+ steps:
+ - name: Set up JDK 25
+ uses: actions/setup-java@v5
+ with:
+ java-version: '25'
+ distribution: 'temurin'
+ cache: maven
\ No newline at end of file
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
deleted file mode 100644
index 8a04a22..0000000
--- a/.github/workflows/maven.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
-
-# This workflow uses actions that are not certified by GitHub.
-# They are provided by a third-party and are governed by
-# separate terms of service, privacy policy, and support
-# documentation.
-
-name: Java CI with Maven
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- - name: Set up JDK 25
- uses: actions/setup-java@v4
- with:
- java-version: '25'
- distribution: 'temurin'
- cache: maven
- - name: Build with Maven
- run: mvn -B package --file pom.xml
-
- # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
- - name: Update dependency graph
- uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
new file mode 100644
index 0000000..f2b4b73
--- /dev/null
+++ b/.github/workflows/workflow.yml
@@ -0,0 +1,36 @@
+name: PR pipeline
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+
+jobs:
+ build:
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Set up JDK
+ uses: ./.github/actions/setup-jdk
+ - name: Build with Maven
+ run: mvn -B package --file pom.xml
+ - name: Update dependency graph
+ uses: advanced-security/maven-dependency-submission-action@v5
+ continue-on-error: true
+ with:
+ directory: .
+ maven-args: "-DmergeClassifiers=true"
+
+ format:
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Set up JDK
+ uses: ./.github/actions/setup-jdk
+ - name: Check formatting
+ run: mvn -B spotless:check --file pom.xml
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 04d477a..16714c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,67 @@ buildNumber.properties
# JDT-specific (Eclipse Java Development Tools)
.classpath
-.vscode
\ No newline at end of file
+.vscode
+.idea
+
+tools/scenicview/
+
+# General
+.DS_Store
+.localized
+__MACOSX/
+.AppleDouble
+.LSOverride
+Icon[
+]
+
+# Resource forks
+._*
+
+# Files and directories that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+.com.apple.timemachine.supported
+.PKInstallSandboxManager
+.PKInstallSandboxManager-SystemSoftware
+.hotfiles.btree
+.vol
+.file
+.disk_label*
+lost+found
+.HFS+ Private Directory Data[
+]
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Mac OS 6 to 9
+Desktop DB
+Desktop DF
+TheFindByContentFolder
+TheVolumeSettingsFolder
+.FBCIndex
+.FBCSemaphoreFile
+.FBCLockFolder
+
+# Quota system
+.quota.group
+.quota.user
+.quota.ops.group
+.quota.ops.user
+
+# TimeMachine
+Backups.backupdb
+.MobileBackups
+.MobileBackups.trash
+MobileBackups.trash
+tmbootpicker.efi
diff --git a/README.md b/README.md
index accc46e..c6f75b3 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,5 @@
-# DesktopApplicationClientJava
\ No newline at end of file
+# DesktopApplicationClientJava
+
+```bash
+git pull --rebase
+```
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 002b18a..b224d9e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,9 +12,7 @@
DesktopApplicationClientJava
DesktopApplicationClientJava
-
- https://github.com/vladcheck/DesktopApplicationClientJava
-
+ https://github.com/vladcheck/DesktopApplicationClientJava
UTF-8
@@ -25,10 +23,57 @@
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.17.0
+
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.17.0
+
+
+ org.testfx
+ openjfx-monocle
+ 21.0.2
+ test
+
+
+ org.testfx
+ testfx-junit5
+ 4.0.18
+ test
+
+
+ org.testfx
+ testfx-core
+ 4.0.18
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.2
+ test
+
+
+ org.hamcrest
+ hamcrest
+ 2.2
+ test
+
org.openjfx
javafx-controls
- 11
+ 27
+
+
+ org.openjfx
+ javafx-fxml
+ 27
org.projectlombok
@@ -36,15 +81,79 @@
1.18.48
provided
+
+ com.dlsc.formsfx
+ formsfx-core
+ 11.6.0
+
+
+ org.openjfx
+ *
+
+
+
+
+ net.synedra
+ validatorfx
+ 0.6.1
+
+
+ org.openjfx
+ *
+
+
+
+
+ org.kordamp.bootstrapfx
+ bootstrapfx-core
+ 0.4.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.11.0
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.4
+
+
+ -Dtestfx.headless=true
+ -Dtestfx.robot=glass
+ -Dglass.platform=Monocle
+ -Dmonocle.platform=Headless
+ -Dprism.order=sw
+ -Dprism.text=t2k
+ -Djava.awt.headless=true
+ --enable-native-access=ALL-UNNAMED
+
+
+
org.apache.maven.plugins
maven-compiler-plugin
3.14.1
+
+
+ org.projectlombok
+ lombok
+ 1.18.48
+
+
+
+ -proc:full
+
25
@@ -56,6 +165,30 @@
com.DesktopApplicationClientJava.Program
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ 3.4.0
+
+
+
+ 1.35.0
+
+
+
+
+
+
+
+
+ spotless-check
+ verify
+
+ check
+
+
+
+
\ No newline at end of file
diff --git a/scenicView.properties b/scenicView.properties
new file mode 100644
index 0000000..8309a28
--- /dev/null
+++ b/scenicView.properties
@@ -0,0 +1,19 @@
+#ScenicView properties
+#Sun Sep 27 00:49:41 MSK 2026
+autoRefreshStyleSheets=false
+automaticScenegraphStructureRefreshing=true
+collapseContainerControls=false
+collapseControls=true
+ignoreMouseTransparentNodes=true
+registerShortcuts=true
+showBaseline=false
+showBounds=true
+showCSSProperties=false
+showDefaultProperties=true
+showFilteredNodesInTree=true
+showInvisibleNodes=false
+showNodesIdInTree=false
+showSearchBar=true
+splitPaneDividerPosition=0.308125
+stageHeight=800.0
+stageWidth=800.0
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java b/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java
new file mode 100644
index 0000000..66afc37
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java
@@ -0,0 +1,543 @@
+package com.DesktopApplicationClientJava.Api;
+
+import com.DesktopApplicationClientJava.Api.Dto.error.ErrorResponse;
+import com.DesktopApplicationClientJava.Api.Dto.request.Admin.RegisterRequest;
+import com.DesktopApplicationClientJava.Api.Dto.request.Admin.UpdateUserRequest;
+import com.DesktopApplicationClientJava.Api.Dto.request.Admin.UserFilterRequest;
+import com.DesktopApplicationClientJava.Api.Dto.request.Auth.LoginRequest;
+import com.DesktopApplicationClientJava.Api.Dto.request.Auth.RefreshTokenRequest;
+import com.DesktopApplicationClientJava.Api.Dto.request.Resource.CreateResourceRequest;
+import com.DesktopApplicationClientJava.Api.Dto.request.Resource.ResourceFilterRequest;
+import com.DesktopApplicationClientJava.Api.Dto.request.Resource.UpdateResourceRequest;
+import com.DesktopApplicationClientJava.Api.Dto.response.FileDto;
+import com.DesktopApplicationClientJava.Api.Dto.response.Jwt.JwtTokenResponse;
+import com.DesktopApplicationClientJava.Api.Dto.response.ResourceDto;
+import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+public class ApiClient {
+ private final HttpClient httpClient;
+ private final ObjectMapper objectMapper;
+ private final String baseUrl;
+
+ private String accessToken;
+ private String refreshToken;
+
+ public ApiClient(String baseUrl) {
+ this.baseUrl = baseUrl;
+ this.httpClient = HttpClient.newHttpClient();
+ this.objectMapper = new ObjectMapper();
+ this.objectMapper.registerModule(new JavaTimeModule());
+ }
+
+ // Вход в систему
+ public JwtTokenResponse login(LoginRequest request) throws IOException, InterruptedException {
+ String jsonBody = objectMapper.writeValueAsString(request);
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/auth/login"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ JwtTokenResponse tokenResponse = parseResponse(response, JwtTokenResponse.class);
+ this.accessToken = tokenResponse.accessToken();
+ this.refreshToken = tokenResponse.refreshToken();
+ return tokenResponse;
+ }
+
+ public JwtTokenResponse refresh() throws IOException, InterruptedException {
+ refreshInternal();
+ return new JwtTokenResponse(accessToken, refreshToken);
+ }
+
+ private void refreshInternal() throws IOException, InterruptedException {
+ if (refreshToken == null) {
+ throw new ApiException(new ErrorResponse("Ошибка: нет refresh токена", 401));
+ }
+
+ String jsonBody = objectMapper.writeValueAsString(new RefreshTokenRequest(refreshToken));
+ HttpRequest request =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/auth/refresh"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
+ .build();
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() >= 400) {
+ throw new ApiException(
+ new ErrorResponse("Refresh failed: " + response.body(), response.statusCode()));
+ }
+
+ JwtTokenResponse tokens = objectMapper.readValue(response.body(), JwtTokenResponse.class);
+ this.accessToken = tokens.accessToken();
+ this.refreshToken = tokens.refreshToken();
+ }
+
+ // Выход из системы
+ public void logout() throws IOException, InterruptedException {
+ if (accessToken == null) {
+ return;
+ }
+ String jsonBody = objectMapper.writeValueAsString(new RefreshTokenRequest(refreshToken));
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/auth/logout"))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ this.accessToken = null;
+ this.refreshToken = null;
+ }
+
+ // Получение информации пользователя о себе
+ public UserDto getMe() throws IOException, InterruptedException {
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/user/me"))
+ .header("Authorization", "Bearer " + accessToken)
+ .GET()
+ .build();
+
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ return parseResponse(response, UserDto.class);
+ }
+
+ // Работа с Ресурсом(Документом)
+ public ResourceDto createResource(CreateResourceRequest request)
+ throws IOException, InterruptedException {
+ String jsonBody = objectMapper.writeValueAsString(request);
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/resource"))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ return parseResponse(response, ResourceDto.class);
+ }
+
+ // Поиск ресурса по Id
+ public ResourceDto findResourceById(UUID id) throws IOException, InterruptedException {
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/resource/" + id))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .GET()
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ return parseResponse(response, ResourceDto.class);
+ }
+
+ // Поиск ресурса по параметрам
+ public List findResourcesByFilters(
+ ResourceFilterRequest filter, Long offset, Long count, String sortBy, String sortDir)
+ throws IOException, InterruptedException {
+ List params = new ArrayList<>();
+ if (filter != null) {
+ if (filter.title() != null && !filter.title().isBlank())
+ params.add("title=" + encode(filter.title()));
+ if (filter.description() != null && !filter.description().isBlank())
+ params.add("description=" + encode(filter.description()));
+ // createdAt/updatedAt пока не отправляем — при необходимости добавим формат
+ }
+ if (offset != null) params.add("offset=" + offset);
+ if (count != null) params.add("count=" + count);
+ if (sortBy != null) params.add("sortBy=" + sortBy);
+ if (sortDir != null) params.add("sortDir=" + sortDir);
+ String url =
+ baseUrl + "/api/v1/resource" + (params.isEmpty() ? "" : "?" + String.join("&", params));
+
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .header("Authorization", "Bearer " + accessToken)
+ .GET()
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+
+ ResourceDto[] arr = objectMapper.readValue(response.body(), ResourceDto[].class);
+ return Arrays.asList(arr);
+ }
+
+ // Обновление ресурса
+ public ResourceDto updateResource(UUID uuid, UpdateResourceRequest request)
+ throws IOException, InterruptedException {
+ String jsonBody = objectMapper.writeValueAsString(request);
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/resource/" + uuid))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .PUT(HttpRequest.BodyPublishers.ofString(jsonBody))
+ .build();
+
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ return parseResponse(response, ResourceDto.class);
+ }
+
+ // Удаление ресурса
+ public void deleteResource(UUID uuid) throws IOException, InterruptedException {
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/resource/" + uuid))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .DELETE()
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ }
+
+ // Создание пользователя
+ public UserDto createUser(RegisterRequest request) throws IOException, InterruptedException {
+ String jsonBody = objectMapper.writeValueAsString(request);
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/admin"))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ return parseResponse(response, UserDto.class);
+ }
+
+ // Поиск пользователя по ID
+ public UserDto getUserById(UUID uuid) throws IOException, InterruptedException {
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/admin/" + uuid))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .GET()
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ return parseResponse(response, UserDto.class);
+ }
+
+ // Поиск пользователя по параметрам
+ public List getUsersByFilters(
+ UserFilterRequest filter, Long offset, Long count, String sortBy, String sortDir)
+ throws IOException, InterruptedException {
+ List params = new ArrayList<>();
+ if (filter != null) {
+ if (filter.email() != null && !filter.email().isBlank())
+ params.add("email=" + encode(filter.email()));
+ if (filter.firstName() != null && !filter.firstName().isBlank())
+ params.add("firstName=" + encode(filter.firstName()));
+ if (filter.lastName() != null && !filter.lastName().isBlank())
+ params.add("lastName=" + encode(filter.lastName()));
+ if (filter.role() != null) params.add("role=" + encode(filter.role().name()));
+ if (filter.enabled() != null) params.add("enabled=" + filter.enabled());
+ }
+
+ if (offset != null) params.add("offset=" + offset);
+ if (count != null) params.add("count=" + count);
+ if (sortBy != null) params.add("sortBy=" + encode(sortBy));
+ if (sortDir != null) params.add("sortDir=" + encode(sortDir));
+
+ String url =
+ baseUrl + "/api/v1/admin" + (params.isEmpty() ? "" : "?" + String.join("&", params));
+
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .header("Authorization", "Bearer " + accessToken)
+ .GET()
+ .build();
+
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ UserDto[] users = objectMapper.readValue(response.body(), UserDto[].class);
+ return Arrays.asList(users);
+ }
+
+ // Обновление пользователя
+ public UserDto updateUser(UUID uuid, UpdateUserRequest request)
+ throws IOException, InterruptedException {
+ String jsonBody = objectMapper.writeValueAsString(request);
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/admin/" + uuid))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .PUT(HttpRequest.BodyPublishers.ofString(jsonBody))
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ return parseResponse(response, UserDto.class);
+ }
+
+ // Удаление пользователя
+ public void deleteUser(UUID uuid) throws IOException, InterruptedException {
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/admin/" + uuid))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .DELETE()
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ }
+
+ public FileDto uploadFile(UUID resourceUuid, Path filePath)
+ throws IOException, InterruptedException {
+ String boundary = "----JavaPksBoundary" + System.currentTimeMillis();
+ byte[] fileBytes = Files.readAllBytes(filePath);
+ String filename = filePath.getFileName().toString();
+ String contentType =
+ Optional.ofNullable(Files.probeContentType(filePath)).orElse("application/octet-stream");
+
+ ByteArrayOutputStream body = new ByteArrayOutputStream();
+
+ body.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
+ body.write(
+ ("Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\n")
+ .getBytes(StandardCharsets.UTF_8));
+ body.write(("Content-Type: " + contentType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8));
+ body.write(fileBytes);
+ body.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
+
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/resource/" + resourceUuid + "/file"))
+ .header("Content-Type", "multipart/form-data; boundary=" + boundary)
+ .header("Authorization", "Bearer " + accessToken)
+ .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
+ .build();
+
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ return parseResponse(response, FileDto.class);
+ }
+
+ public Path downloadFile(UUID resourceUuid, UUID fileUuid, Path destinationDir)
+ throws IOException, InterruptedException {
+
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/resource/" + resourceUuid + "/file/" + fileUuid))
+ .header("Authorization", "Bearer " + accessToken)
+ .GET()
+ .build();
+
+ HttpResponse response =
+ executeWithRetry(httpRequest, HttpResponse.BodyHandlers.ofInputStream());
+
+ if (response.statusCode() >= 400) {
+ String body = new String(response.body().readAllBytes(), StandardCharsets.UTF_8);
+ response.body().close();
+ try {
+ ErrorResponse error = objectMapper.readValue(body, ErrorResponse.class);
+ throw new ApiException(error);
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception parseFail) {
+ throw new ApiException(new ErrorResponse("Ошибка сервера: " + body, response.statusCode()));
+ }
+ }
+
+ String filename =
+ parseFilename(response.headers().firstValue("Content-Disposition").orElse(null));
+ if (filename == null || filename.isBlank()) {
+ filename = fileUuid.toString();
+ }
+
+ Files.createDirectories(destinationDir);
+ Path target = destinationDir.resolve(filename);
+
+ try (InputStream in = response.body()) {
+ Files.copy(in, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+ }
+ return target;
+ }
+
+ public void deleteFile(UUID resourceUuid, UUID fileUuid)
+ throws IOException, InterruptedException {
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/api/v1/resource/" + resourceUuid + "/file/" + fileUuid))
+ .header("Authorization", "Bearer " + accessToken)
+ .DELETE()
+ .build();
+ HttpResponse response = send(httpRequest);
+ checkError(response);
+ }
+
+ private static String parseFilename(String cd) {
+ if (cd == null) return null;
+
+ int starIdx = cd.indexOf("filename*=");
+ if (starIdx >= 0) {
+ String val = cd.substring(starIdx + "filename*=".length()).trim();
+ int semi = val.indexOf(';');
+ if (semi >= 0) val = val.substring(0, semi).trim();
+ int encIdx = val.indexOf("''");
+ if (encIdx >= 0) {
+ String encoded = val.substring(encIdx + 2);
+ return URLDecoder.decode(encoded, StandardCharsets.UTF_8);
+ }
+ }
+
+ int idx = cd.indexOf("filename=");
+ if (idx >= 0) {
+ String val = cd.substring(idx + "filename=".length()).trim();
+ if (val.startsWith("\"")) {
+ int end = val.indexOf('"', 1);
+ if (end > 0) return val.substring(1, end);
+ } else {
+ int semi = val.indexOf(';');
+ if (semi >= 0) val = val.substring(0, semi);
+ return val.trim();
+ }
+ }
+ return null;
+ }
+
+ // Выгрузка данных (пользователей, документов и их файлов)
+ public byte[] exportUsers() throws IOException, InterruptedException {
+ return downloadBinary("/api/v1/admin/users/export");
+ }
+
+ public byte[] exportResources() throws IOException, InterruptedException {
+ return downloadBinary("/api/v1/admin/resource/export");
+ }
+
+ public byte[] exportResourceFiles(UUID resourceId) throws IOException, InterruptedException {
+ return downloadBinary("/api/v1/admin/resource/" + resourceId + "/files/export");
+ }
+
+ private byte[] downloadBinary(String path) throws IOException, InterruptedException {
+ HttpRequest httpRequest =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + path))
+ .header("Authorization", "Bearer " + accessToken)
+ .GET()
+ .build();
+
+ HttpResponse response =
+ executeWithRetry(httpRequest, HttpResponse.BodyHandlers.ofByteArray());
+
+ if (response.statusCode() >= 400) {
+ String body = new String(response.body(), StandardCharsets.UTF_8);
+ try {
+ ErrorResponse error = objectMapper.readValue(body, ErrorResponse.class);
+ throw new ApiException(error);
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception parseFail) {
+ throw new ApiException(new ErrorResponse("Ошибка сервера: " + body, response.statusCode()));
+ }
+ }
+ return response.body();
+ }
+
+ private HttpResponse send(HttpRequest request) throws IOException, InterruptedException {
+ return executeWithRetry(request, HttpResponse.BodyHandlers.ofString());
+ }
+
+ // Проверка ошибок сервера
+ private void checkError(HttpResponse response) {
+ if (response.statusCode() < 400) return;
+ try {
+ ErrorResponse error = objectMapper.readValue(response.body(), ErrorResponse.class);
+ throw new ApiException(error);
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception parseFail) {
+ throw new ApiException(
+ new ErrorResponse("Ошибка сервера: " + response.body(), response.statusCode()));
+ }
+ }
+
+ // Парсер ответа
+ private T parseResponse(HttpResponse response, Class clazz) throws IOException {
+ if (response.body() == null || response.body().isBlank()) {
+ return null;
+ }
+ return objectMapper.readValue(response.body(), clazz);
+ }
+
+ private HttpResponse executeWithRetry(
+ HttpRequest request, HttpResponse.BodyHandler handler)
+ throws IOException, InterruptedException {
+
+ HttpResponse response = httpClient.send(request, handler);
+
+ if (response.statusCode() != 401) return response;
+
+ if (request.uri().getPath().startsWith("/api/v1/auth/")) return response;
+
+ if (refreshToken == null) return response;
+
+ try {
+ refreshInternal();
+ } catch (Exception e) {
+ // refresh не удался — отдаём оригинальный 401, вызывающий код разберётся
+ return response;
+ }
+
+ // повторяем исходный запрос с новым access-токеном
+ HttpRequest retried =
+ HttpRequest.newBuilder(request, (name, value) -> !name.equalsIgnoreCase("Authorization"))
+ .header("Authorization", "Bearer " + accessToken)
+ .build();
+
+ return httpClient.send(retried, handler);
+ }
+
+ private static String encode(String value) {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8);
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+
+ public ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/ApiConfig.java b/src/main/java/com/DesktopApplicationClientJava/Api/ApiConfig.java
new file mode 100644
index 0000000..e434338
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/ApiConfig.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.Api;
+
+public class ApiConfig {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/ApiException.java b/src/main/java/com/DesktopApplicationClientJava/Api/ApiException.java
new file mode 100644
index 0000000..06ba69f
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/ApiException.java
@@ -0,0 +1,20 @@
+package com.DesktopApplicationClientJava.Api;
+
+import com.DesktopApplicationClientJava.Api.Dto.error.ErrorResponse;
+
+public class ApiException extends RuntimeException {
+ private final ErrorResponse errorResponse;
+
+ public ApiException(ErrorResponse errorResponse) {
+ super(errorResponse.message());
+ this.errorResponse = errorResponse;
+ }
+
+ public ErrorResponse getErrorResponse() {
+ return errorResponse;
+ }
+
+ public int getStatus() {
+ return errorResponse.status();
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/error/ErrorResponse.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/error/ErrorResponse.java
new file mode 100644
index 0000000..9a563cb
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/error/ErrorResponse.java
@@ -0,0 +1,7 @@
+package com.DesktopApplicationClientJava.Api.Dto.error;
+
+public record ErrorResponse(String message, int status, long timestamp) {
+ public ErrorResponse(String message, int status) {
+ this(message, status, System.currentTimeMillis());
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/RegisterRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/RegisterRequest.java
new file mode 100644
index 0000000..43d8021
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/RegisterRequest.java
@@ -0,0 +1,10 @@
+package com.DesktopApplicationClientJava.Api.Dto.request.Admin;
+
+import com.DesktopApplicationClientJava.entities.Role;
+
+public record RegisterRequest(
+ String email,
+ String password,
+ Role role, // "USER", "MODER" или "ADMIN"
+ String firstname,
+ String lastname) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UpdateUserRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UpdateUserRequest.java
new file mode 100644
index 0000000..4ac2708
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UpdateUserRequest.java
@@ -0,0 +1,6 @@
+package com.DesktopApplicationClientJava.Api.Dto.request.Admin;
+
+import com.DesktopApplicationClientJava.entities.Role;
+
+public record UpdateUserRequest(
+ String email, Boolean enabled, Role role, String firstname, String lastname) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UserFilterRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UserFilterRequest.java
new file mode 100644
index 0000000..cb3af19
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UserFilterRequest.java
@@ -0,0 +1,13 @@
+package com.DesktopApplicationClientJava.Api.Dto.request.Admin;
+
+import com.DesktopApplicationClientJava.entities.Role;
+import java.time.LocalDateTime;
+
+public record UserFilterRequest(
+ String email,
+ Role role,
+ String firstName,
+ String lastName,
+ Boolean enabled,
+ LocalDateTime createdAt,
+ LocalDateTime updatedAt) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/LoginRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/LoginRequest.java
new file mode 100644
index 0000000..95cddd9
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/LoginRequest.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.Api.Dto.request.Auth;
+
+public record LoginRequest(String email, String password) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/RefreshTokenRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/RefreshTokenRequest.java
new file mode 100644
index 0000000..7543162
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/RefreshTokenRequest.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.Api.Dto.request.Auth;
+
+public record RefreshTokenRequest(String refreshToken) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/CreateResourceRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/CreateResourceRequest.java
new file mode 100644
index 0000000..19672e1
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/CreateResourceRequest.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.Api.Dto.request.Resource;
+
+public record CreateResourceRequest(String title, String description) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/ResourceFilterRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/ResourceFilterRequest.java
new file mode 100644
index 0000000..3333123
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/ResourceFilterRequest.java
@@ -0,0 +1,6 @@
+package com.DesktopApplicationClientJava.Api.Dto.request.Resource;
+
+import java.time.LocalDateTime;
+
+public record ResourceFilterRequest(
+ String title, String description, LocalDateTime createdAt, LocalDateTime updatedAt) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/UpdateResourceRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/UpdateResourceRequest.java
new file mode 100644
index 0000000..ceffbc2
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/UpdateResourceRequest.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.Api.Dto.request.Resource;
+
+public record UpdateResourceRequest(String title, String description) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/FileDto.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/FileDto.java
new file mode 100644
index 0000000..5facf5e
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/FileDto.java
@@ -0,0 +1,5 @@
+package com.DesktopApplicationClientJava.Api.Dto.response;
+
+import java.util.UUID;
+
+public record FileDto(UUID uuid, String name, String contentType, Long size) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/Jwt/JwtTokenResponse.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/Jwt/JwtTokenResponse.java
new file mode 100644
index 0000000..5e4b709
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/Jwt/JwtTokenResponse.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.Api.Dto.response.Jwt;
+
+public record JwtTokenResponse(String accessToken, String refreshToken) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/ResourceDto.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/ResourceDto.java
new file mode 100644
index 0000000..cb4d46e
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/ResourceDto.java
@@ -0,0 +1,13 @@
+package com.DesktopApplicationClientJava.Api.Dto.response;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+public record ResourceDto(
+ UUID uuid,
+ String title,
+ String description,
+ LocalDateTime createdAt,
+ LocalDateTime updatedAt,
+ List files) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/StoredFile.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/StoredFile.java
new file mode 100644
index 0000000..383680b
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/StoredFile.java
@@ -0,0 +1,15 @@
+package com.DesktopApplicationClientJava.Api.Dto.response;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public record StoredFile(InputStream content, String contentType, long size)
+ implements AutoCloseable {
+
+ @Override
+ public void close() throws IOException {
+ if (content != null) {
+ content.close();
+ }
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java
new file mode 100644
index 0000000..ea2d3a9
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java
@@ -0,0 +1,15 @@
+package com.DesktopApplicationClientJava.Api.Dto.response;
+
+import com.DesktopApplicationClientJava.entities.Role;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+public record UserDto(
+ UUID uuid,
+ String email,
+ Role role,
+ String firstName,
+ String lastName,
+ Boolean enabled,
+ LocalDateTime createdAt,
+ LocalDateTime updatedAt) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Main.java b/src/main/java/com/DesktopApplicationClientJava/Main.java
new file mode 100644
index 0000000..249f023
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/Main.java
@@ -0,0 +1,9 @@
+package com.DesktopApplicationClientJava;
+
+import javafx.application.Application;
+
+public class Main {
+ public static void main(String[] args) {
+ Application.launch(Program.class, args);
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/Program.java b/src/main/java/com/DesktopApplicationClientJava/Program.java
index deea9b6..c5bbe90 100644
--- a/src/main/java/com/DesktopApplicationClientJava/Program.java
+++ b/src/main/java/com/DesktopApplicationClientJava/Program.java
@@ -1,30 +1,38 @@
package com.DesktopApplicationClientJava;
+import com.DesktopApplicationClientJava.session.Session;
+import com.DesktopApplicationClientJava.utils.ControllerWiring;
+import java.io.IOException;
import javafx.application.Application;
-import javafx.scene.Group;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
import javafx.scene.Scene;
-import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Program extends Application {
- public static void main(String[] args) {
- launch(args);
- }
+ private final Session session = new Session();
+ private static final double windowWidthPx = 400.0;
+ private static final double windowHeightPx = 400.0;
- @Override
- public void start(Stage stage) {
- Text text = new Text("Hello JAVAFX!");
- text.setLayoutX(80);
- text.setLayoutY(80);
+ @Override
+ public void start(Stage stage) throws IOException {
+ FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/LoginForm.fxml"));
+ Parent root = loader.load();
+ Scene scene = new Scene(root);
+ loadStyleSheet(scene, "/styles.css");
- Group group = new Group(text);
- Scene scene = new Scene(group);
+ ControllerWiring.wire(loader.getController(), stage, session);
- stage.setScene(scene);
- stage.setTitle("JavaFX Application");
- stage.setWidth(300);
- stage.setHeight(250);
+ stage.setScene(scene);
+ stage.setTitle("JavaFX Application");
+ stage.setWidth(windowWidthPx);
+ stage.setHeight(windowHeightPx);
+ stage.centerOnScreen();
- stage.show();
- }
-}
\ No newline at end of file
+ stage.show();
+ }
+
+ private void loadStyleSheet(Scene scene, String path) {
+ scene.getStylesheets().add(getClass().getResource(path).toExternalForm());
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java
new file mode 100644
index 0000000..d8baf11
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java
@@ -0,0 +1,108 @@
+package com.DesktopApplicationClientJava.controllers;
+
+import com.DesktopApplicationClientJava.controllers.utils.Controller;
+import com.DesktopApplicationClientJava.entities.User;
+import com.DesktopApplicationClientJava.utils.FieldError;
+import com.DesktopApplicationClientJava.validation.EmailValidator;
+import com.DesktopApplicationClientJava.validation.FirstNameValidator;
+import com.DesktopApplicationClientJava.validation.LastNameValidator;
+import com.DesktopApplicationClientJava.validation.PasswordValidator;
+import com.DesktopApplicationClientJava.validation.ThirdNameValidator;
+import com.DesktopApplicationClientJava.validation.utils.Validator;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.TextField;
+
+public class LoginFormController extends Controller {
+ @FXML private TextField firstNameField;
+ @FXML private Label firstNameErrorLabel;
+ private FieldError firstNameError;
+
+ @FXML private TextField lastNameField;
+ @FXML private Label lastNameErrorLabel;
+ private FieldError lastNameError;
+
+ @FXML private TextField thirdNameField;
+ @FXML private Label thirdNameErrorLabel;
+ private FieldError thirdNameError;
+
+ @FXML private TextField emailField;
+ @FXML private Label emailErrorLabel;
+ private FieldError emailError;
+
+ @FXML private PasswordField passwordField;
+ @FXML private Label passwordErrorLabel;
+ private FieldError passwordError;
+
+ @FXML private Button submitButton;
+
+ @FXML
+ private void initialize() {
+ firstNameError = new FieldError(firstNameErrorLabel);
+ lastNameError = new FieldError(lastNameErrorLabel);
+ thirdNameError = new FieldError(thirdNameErrorLabel);
+ emailError = new FieldError(emailErrorLabel);
+ passwordError = new FieldError(passwordErrorLabel);
+
+ emailField.textProperty().addListener((o, a, b) -> emailError.hide());
+ passwordField.textProperty().addListener((o, a, b) -> passwordError.hide());
+ }
+
+ @FXML
+ private void onSubmit(ActionEvent event) {
+ if (!isValid()) return;
+
+ User user =
+ authenticate(firstNameField.getText(), lastNameField.getText(), emailField.getText());
+ if (user == null) return;
+
+ session.setCurrentUser(user);
+ navigator.goTo("/fxml/Search.fxml");
+ }
+
+ private boolean isValid() {
+ return isFirstNameValid()
+ & isLastNameValid()
+ & isThirdNameValid()
+ & isEmailValid()
+ & isPasswordValid();
+ }
+
+ private boolean isFirstNameValid() {
+ return isFieldValid(firstNameField, new FirstNameValidator(), firstNameError);
+ }
+
+ private boolean isLastNameValid() {
+ return isFieldValid(lastNameField, new LastNameValidator(), lastNameError);
+ }
+
+ private boolean isThirdNameValid() {
+ return isFieldValid(thirdNameField, new ThirdNameValidator(), thirdNameError);
+ }
+
+ private boolean isEmailValid() {
+ return isFieldValid(emailField, new EmailValidator(), emailError);
+ }
+
+ private boolean isPasswordValid() {
+ return isFieldValid(passwordField, new PasswordValidator(), passwordError);
+ }
+
+ private boolean isFieldValid(TextField field, Validator validator, FieldError errorLabel) {
+ String input = field.getText();
+ var error = validator.isValid(input);
+ if (error == null) {
+ errorLabel.hide();
+ return true;
+ }
+ errorLabel.show(error);
+ return false;
+ }
+
+ private User authenticate(String firstName, String lastName, String email) {
+ return new User(firstName, lastName, email);
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/SearchController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/SearchController.java
new file mode 100644
index 0000000..9112fdd
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/controllers/SearchController.java
@@ -0,0 +1,5 @@
+package com.DesktopApplicationClientJava.controllers;
+
+import com.DesktopApplicationClientJava.controllers.utils.Controller;
+
+public class SearchController extends Controller {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java
new file mode 100644
index 0000000..34b2bc4
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java
@@ -0,0 +1,19 @@
+package com.DesktopApplicationClientJava.controllers.utils;
+
+import com.DesktopApplicationClientJava.navigation.Navigator;
+import com.DesktopApplicationClientJava.navigation.NavigatorAware;
+import com.DesktopApplicationClientJava.session.Session;
+import com.DesktopApplicationClientJava.session.SessionAware;
+
+public abstract class Controller implements SessionAware, NavigatorAware {
+ protected Session session;
+ protected Navigator navigator;
+
+ public void setSession(Session session) {
+ this.session = session;
+ }
+
+ public void setNavigator(Navigator navigator) {
+ this.navigator = navigator;
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/entities/Role.java b/src/main/java/com/DesktopApplicationClientJava/entities/Role.java
new file mode 100644
index 0000000..992a4ed
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/entities/Role.java
@@ -0,0 +1,7 @@
+package com.DesktopApplicationClientJava.entities;
+
+public enum Role {
+ USER,
+ MODER,
+ ADMIN
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/entities/User.java b/src/main/java/com/DesktopApplicationClientJava/entities/User.java
new file mode 100644
index 0000000..dfc6d99
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/entities/User.java
@@ -0,0 +1,15 @@
+package com.DesktopApplicationClientJava.entities;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+/** User */
+@AllArgsConstructor
+@Getter
+@Setter
+public class User {
+ private String firstName;
+ private String lastName;
+ private String email;
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/navigation/Navigator.java b/src/main/java/com/DesktopApplicationClientJava/navigation/Navigator.java
new file mode 100644
index 0000000..3f84fc7
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/navigation/Navigator.java
@@ -0,0 +1,38 @@
+package com.DesktopApplicationClientJava.navigation;
+
+import com.DesktopApplicationClientJava.session.Session;
+import com.DesktopApplicationClientJava.session.SessionAware;
+import java.io.IOException;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.stage.Stage;
+import lombok.AllArgsConstructor;
+
+/** Navigator */
+@AllArgsConstructor
+public class Navigator {
+ private final Stage stage;
+ private final Session session;
+
+ public void goTo(String fxmlFile) {
+ var url = Navigator.class.getResource(fxmlFile);
+ if (url == null) {
+ throw new IllegalArgumentException("FXML not found on classpath: " + fxmlFile);
+ }
+
+ FXMLLoader loader = new FXMLLoader(url);
+ try {
+ Parent root = loader.load();
+ var controller = loader.getController();
+ if (controller instanceof SessionAware sa) {
+ sa.setSession(session);
+ }
+ if (controller instanceof NavigatorAware na) {
+ na.setNavigator(this);
+ }
+ stage.getScene().setRoot(root);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load FXML: " + fxmlFile, e);
+ }
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/navigation/NavigatorAware.java b/src/main/java/com/DesktopApplicationClientJava/navigation/NavigatorAware.java
new file mode 100644
index 0000000..f4a4bc8
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/navigation/NavigatorAware.java
@@ -0,0 +1,5 @@
+package com.DesktopApplicationClientJava.navigation;
+
+public interface NavigatorAware {
+ void setNavigator(Navigator navigator);
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/services/AuthService.java b/src/main/java/com/DesktopApplicationClientJava/services/AuthService.java
new file mode 100644
index 0000000..40667ff
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/services/AuthService.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.services;
+
+public class AuthService {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/services/FileService.java b/src/main/java/com/DesktopApplicationClientJava/services/FileService.java
new file mode 100644
index 0000000..6f616cf
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/services/FileService.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.services;
+
+public class FileService {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java b/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java
new file mode 100644
index 0000000..cabf91c
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.services;
+
+public class ResourceService {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/services/UsersService.java b/src/main/java/com/DesktopApplicationClientJava/services/UsersService.java
new file mode 100644
index 0000000..018573e
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/services/UsersService.java
@@ -0,0 +1,3 @@
+package com.DesktopApplicationClientJava.services;
+
+public class UsersService {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/session/Session.java b/src/main/java/com/DesktopApplicationClientJava/session/Session.java
new file mode 100644
index 0000000..352b636
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/session/Session.java
@@ -0,0 +1,21 @@
+package com.DesktopApplicationClientJava.session;
+
+import com.DesktopApplicationClientJava.entities.User;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+public class Session {
+ private User currentUser;
+
+ public User getCurrentUser() {
+ return currentUser;
+ }
+
+ public void setCurrentUser(User user) {
+ this.currentUser = user;
+ }
+
+ public boolean isLoggedIn() {
+ return currentUser != null;
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/session/SessionAware.java b/src/main/java/com/DesktopApplicationClientJava/session/SessionAware.java
new file mode 100644
index 0000000..83a8e4f
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/session/SessionAware.java
@@ -0,0 +1,5 @@
+package com.DesktopApplicationClientJava.session;
+
+public interface SessionAware {
+ void setSession(Session session);
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/utils/ControllerWiring.java b/src/main/java/com/DesktopApplicationClientJava/utils/ControllerWiring.java
new file mode 100644
index 0000000..f986f72
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/utils/ControllerWiring.java
@@ -0,0 +1,16 @@
+package com.DesktopApplicationClientJava.utils;
+
+import com.DesktopApplicationClientJava.navigation.Navigator;
+import com.DesktopApplicationClientJava.navigation.NavigatorAware;
+import com.DesktopApplicationClientJava.session.Session;
+import com.DesktopApplicationClientJava.session.SessionAware;
+import javafx.stage.Stage;
+
+public final class ControllerWiring {
+ private ControllerWiring() {}
+
+ public static void wire(Object controller, Stage stage, Session session) {
+ if (controller instanceof SessionAware sa) sa.setSession(session);
+ if (controller instanceof NavigatorAware na) na.setNavigator(new Navigator(stage, session));
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/utils/FieldError.java b/src/main/java/com/DesktopApplicationClientJava/utils/FieldError.java
new file mode 100644
index 0000000..cafa2bf
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/utils/FieldError.java
@@ -0,0 +1,28 @@
+package com.DesktopApplicationClientJava.utils;
+
+import javafx.scene.control.Label;
+
+public class FieldError {
+ private final Label label;
+
+ public FieldError(Label label) {
+ this.label = label;
+ }
+
+ public void show(String message) {
+ if (label == null) return;
+ label.setText(message);
+ label.setVisible(true);
+ label.setManaged(true);
+ }
+
+ public void hide() {
+ if (label == null) return;
+ label.setVisible(false);
+ label.setManaged(false);
+ }
+
+ public boolean isShowing() {
+ return label.isVisible();
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/EmailValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/EmailValidator.java
new file mode 100644
index 0000000..75e0d99
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/EmailValidator.java
@@ -0,0 +1,21 @@
+package com.DesktopApplicationClientJava.validation;
+
+import com.DesktopApplicationClientJava.validation.utils.AbstractValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers;
+
+public class EmailValidator extends AbstractValidator {
+ public static final String FIELD_NAME = "Почта";
+ public static final int MIN_EMAIL_LENGTH = 5;
+ public static final int MAX_EMAIL_LENGTH = 100;
+
+ public String isValid(String input) {
+ if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required;
+ if (ValidatorHelpers.isShorterThan(input, MIN_EMAIL_LENGTH))
+ return ErrorMessages.MinLength(FIELD_NAME, MIN_EMAIL_LENGTH);
+ if (ValidatorHelpers.isLongerThan(input, MAX_EMAIL_LENGTH))
+ return ErrorMessages.MaxLength(FIELD_NAME, MAX_EMAIL_LENGTH);
+ if (!ValidatorHelpers.includes(input, "@")) return ErrorMessages.IncorrectFormat;
+ return null;
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/FirstNameValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/FirstNameValidator.java
new file mode 100644
index 0000000..5f9516f
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/FirstNameValidator.java
@@ -0,0 +1,17 @@
+package com.DesktopApplicationClientJava.validation;
+
+import com.DesktopApplicationClientJava.validation.utils.AbstractValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers;
+
+public class FirstNameValidator extends AbstractValidator {
+ public static final String FIELD_NAME = "Имя";
+ private static final int MIN_LENGTH = 2;
+
+ public String isValid(String input) {
+ if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required;
+ if (ValidatorHelpers.isShorterThan(input, MIN_LENGTH))
+ return ErrorMessages.MinLength(FIELD_NAME, MIN_LENGTH);
+ return null;
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/LastNameValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/LastNameValidator.java
new file mode 100644
index 0000000..933e658
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/LastNameValidator.java
@@ -0,0 +1,17 @@
+package com.DesktopApplicationClientJava.validation;
+
+import com.DesktopApplicationClientJava.validation.utils.AbstractValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers;
+
+public class LastNameValidator extends AbstractValidator {
+ public static final String FIELD_NAME = "Фамилия";
+ public static final int MIN_LENGTH = 2;
+
+ public String isValid(String input) {
+ if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required;
+ if (ValidatorHelpers.isShorterThan(input, MIN_LENGTH))
+ return ErrorMessages.MinLength(FIELD_NAME, MIN_LENGTH);
+ return null;
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/PasswordValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/PasswordValidator.java
new file mode 100644
index 0000000..a9e2c97
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/PasswordValidator.java
@@ -0,0 +1,20 @@
+package com.DesktopApplicationClientJava.validation;
+
+import com.DesktopApplicationClientJava.validation.utils.AbstractValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers;
+
+public class PasswordValidator extends AbstractValidator {
+ public static final String FIELD_NAME = "Пароль";
+ public static final int MIN_LENGTH = 8;
+ public static final int MAX_LENGTH = 64;
+
+ public String isValid(String input) {
+ if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required;
+ if (ValidatorHelpers.isShorterThan(input, MIN_LENGTH))
+ return ErrorMessages.MinLength(FIELD_NAME, MIN_LENGTH);
+ if (ValidatorHelpers.isLongerThan(input, MAX_LENGTH))
+ return ErrorMessages.MaxLength(FIELD_NAME, MAX_LENGTH);
+ return null;
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/ThirdNameValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/ThirdNameValidator.java
new file mode 100644
index 0000000..fd3aa2f
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/ThirdNameValidator.java
@@ -0,0 +1,15 @@
+package com.DesktopApplicationClientJava.validation;
+
+import com.DesktopApplicationClientJava.validation.utils.AbstractValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers;
+
+public class ThirdNameValidator extends AbstractValidator {
+ public static final String FIELD_NAME = "Отчество";
+
+ @Override
+ public String isValid(String input) {
+ if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required;
+ return null;
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/utils/AbstractValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/utils/AbstractValidator.java
new file mode 100644
index 0000000..3a921e8
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/utils/AbstractValidator.java
@@ -0,0 +1,5 @@
+package com.DesktopApplicationClientJava.validation.utils;
+
+public abstract class AbstractValidator implements Validator {
+ public abstract String isValid(String input);
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/utils/ErrorMessages.java b/src/main/java/com/DesktopApplicationClientJava/validation/utils/ErrorMessages.java
new file mode 100644
index 0000000..51a3b71
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/utils/ErrorMessages.java
@@ -0,0 +1,14 @@
+package com.DesktopApplicationClientJava.validation.utils;
+
+public class ErrorMessages {
+ public static final String Required = "Это поле обязательно для заполнения";
+ public static final String IncorrectFormat = "Неверный формат";
+
+ public static final String MinLength(String fieldName, int minLength) {
+ return fieldName + " не может быть короче, чем " + minLength + " символов";
+ }
+
+ public static final String MaxLength(String fieldName, int maxLength) {
+ return fieldName + " не может быть длиннее, чем " + maxLength + " символов";
+ }
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/utils/Validator.java b/src/main/java/com/DesktopApplicationClientJava/validation/utils/Validator.java
new file mode 100644
index 0000000..081eef7
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/utils/Validator.java
@@ -0,0 +1,6 @@
+package com.DesktopApplicationClientJava.validation.utils;
+
+@FunctionalInterface
+public interface Validator {
+ String isValid(String input);
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/utils/ValidatorHelpers.java b/src/main/java/com/DesktopApplicationClientJava/validation/utils/ValidatorHelpers.java
new file mode 100644
index 0000000..6db826b
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/validation/utils/ValidatorHelpers.java
@@ -0,0 +1,25 @@
+package com.DesktopApplicationClientJava.validation.utils;
+
+public class ValidatorHelpers {
+ public static boolean isEmpty(String input) {
+ return input == null || input.isBlank();
+ }
+
+ public static boolean isShorterThan(String input, int min) {
+ return input.length() < min;
+ }
+
+ public static boolean isLongerThan(String input, int max) {
+ return input.length() > max;
+ }
+
+ /**
+ * @param input input string
+ * @param sub substring
+ * @return true if input includes a substring sub,
+ * false otherwise
+ */
+ public static boolean includes(String input, String sub) {
+ return input.contains(sub);
+ }
+}
diff --git a/src/main/resources/fxml/LoginForm.fxml b/src/main/resources/fxml/LoginForm.fxml
new file mode 100644
index 0000000..34fbb3c
--- /dev/null
+++ b/src/main/resources/fxml/LoginForm.fxml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/fxml/Search.fxml b/src/main/resources/fxml/Search.fxml
new file mode 100644
index 0000000..b6b8878
--- /dev/null
+++ b/src/main/resources/fxml/Search.fxml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/styles.css b/src/main/resources/styles.css
new file mode 100644
index 0000000..7283338
--- /dev/null
+++ b/src/main/resources/styles.css
@@ -0,0 +1,42 @@
+.i {
+ -fx-font-style: italic;
+}
+
+.b {
+ -fx-font-weight: bold;
+}
+
+.h1 {
+ -fx-font-size: 32px;
+ -fx-font-weight: bold;
+}
+
+.h2 {
+ -fx-font-size: 24px;
+ -fx-font-weight: bold;
+}
+
+.h3 {
+ -fx-font-size: 19px;
+ -fx-font-weight: bold;
+}
+
+.h4 {
+ -fx-font-size: 16px;
+ -fx-font-weight: bold;
+}
+
+.h5 {
+ -fx-font-size: 13px;
+ -fx-font-weight: bold;
+}
+
+.h6 {
+ -fx-font-size: 11px;
+ -fx-font-weight: bold;
+}
+
+.error-label {
+ -fx-text-fill: red;
+ -fx-font-size: 11px;
+}
\ No newline at end of file
diff --git a/src/test/java/validation/EmailValidatorTest.java b/src/test/java/validation/EmailValidatorTest.java
new file mode 100644
index 0000000..54f5480
--- /dev/null
+++ b/src/test/java/validation/EmailValidatorTest.java
@@ -0,0 +1,48 @@
+package validation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.DesktopApplicationClientJava.validation.EmailValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import org.junit.jupiter.api.Test;
+
+public class EmailValidatorTest {
+ private static final String FIELD_NAME = EmailValidator.FIELD_NAME;
+ private static final EmailValidator emailValidator = new EmailValidator();
+
+ @Test
+ void emptyEmailIsRejected() {
+ assertEquals(ErrorMessages.Required, emailValidator.isValid(""));
+ }
+
+ @Test
+ void blankEmailIsRejected() {
+ assertEquals(ErrorMessages.Required, emailValidator.isValid(" "));
+ }
+
+ @Test
+ void shortEmailIsRejected() {
+ assertEquals(
+ ErrorMessages.MinLength(FIELD_NAME, EmailValidator.MIN_EMAIL_LENGTH),
+ emailValidator.isValid("a@b"));
+ }
+
+ @Test
+ void longEmailIsRejected() {
+ String e = "a".repeat(95) + "@x.com";
+ assertEquals(
+ ErrorMessages.MaxLength(FIELD_NAME, EmailValidator.MAX_EMAIL_LENGTH),
+ emailValidator.isValid(e));
+ }
+
+ @Test
+ void emailWithoutAtIsRejected() {
+ assertEquals(ErrorMessages.IncorrectFormat, emailValidator.isValid("nope.com"));
+ }
+
+ @Test
+ void validEmailIsAccepted() {
+ assertNull(emailValidator.isValid("test@example.com"));
+ }
+}
diff --git a/src/test/java/validation/FirstNameValidatorTest.java b/src/test/java/validation/FirstNameValidatorTest.java
new file mode 100644
index 0000000..1ef2a3a
--- /dev/null
+++ b/src/test/java/validation/FirstNameValidatorTest.java
@@ -0,0 +1,23 @@
+package validation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.DesktopApplicationClientJava.validation.FirstNameValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import org.junit.jupiter.api.Test;
+
+public class FirstNameValidatorTest {
+ private static final String FIELD_NAME = FirstNameValidator.FIELD_NAME;
+ private static final FirstNameValidator firstNameValidator = new FirstNameValidator();
+
+ @Test
+ void emptyFirstNameIsRejected() {
+ assertEquals(ErrorMessages.Required, firstNameValidator.isValid(""));
+ }
+
+ @Test
+ void validFirstNameIsAccepted() {
+ assertNull(firstNameValidator.isValid("Антон"));
+ }
+}
diff --git a/src/test/java/validation/LastNameValidatorTest.java b/src/test/java/validation/LastNameValidatorTest.java
new file mode 100644
index 0000000..aeec56e
--- /dev/null
+++ b/src/test/java/validation/LastNameValidatorTest.java
@@ -0,0 +1,23 @@
+package validation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.DesktopApplicationClientJava.validation.LastNameValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import org.junit.jupiter.api.Test;
+
+public class LastNameValidatorTest {
+ private static final String FIELD_NAME = LastNameValidator.FIELD_NAME;
+ private static final LastNameValidator lastNameValidator = new LastNameValidator();
+
+ @Test
+ void emptyLastNameIsRejected() {
+ assertEquals(ErrorMessages.Required, lastNameValidator.isValid(""));
+ }
+
+ @Test
+ void validLastNameIsAccepted() {
+ assertNull(lastNameValidator.isValid("Антонов"));
+ }
+}
diff --git a/src/test/java/validation/PasswordValidatorTest.java b/src/test/java/validation/PasswordValidatorTest.java
new file mode 100644
index 0000000..8350694
--- /dev/null
+++ b/src/test/java/validation/PasswordValidatorTest.java
@@ -0,0 +1,30 @@
+package validation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.DesktopApplicationClientJava.validation.PasswordValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import org.junit.jupiter.api.Test;
+
+public class PasswordValidatorTest {
+ private static final String FIELD_NAME = PasswordValidator.FIELD_NAME;
+ private static final PasswordValidator passwordValidator = new PasswordValidator();
+
+ @Test
+ void emptyPasswordIsRejected() {
+ assertEquals(ErrorMessages.Required, passwordValidator.isValid(""));
+ }
+
+ @Test
+ void shortPasswordIsRejected() {
+ assertEquals(
+ ErrorMessages.MinLength(FIELD_NAME, PasswordValidator.MIN_LENGTH),
+ passwordValidator.isValid("short"));
+ }
+
+ @Test
+ void exactlyEightCharPasswordIsAccepted() {
+ assertNull(passwordValidator.isValid("12345678"));
+ }
+}
diff --git a/src/test/java/validation/ThirdNameValidatorTest.java b/src/test/java/validation/ThirdNameValidatorTest.java
new file mode 100644
index 0000000..0eb4f25
--- /dev/null
+++ b/src/test/java/validation/ThirdNameValidatorTest.java
@@ -0,0 +1,23 @@
+package validation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.DesktopApplicationClientJava.validation.ThirdNameValidator;
+import com.DesktopApplicationClientJava.validation.utils.ErrorMessages;
+import org.junit.jupiter.api.Test;
+
+public class ThirdNameValidatorTest {
+ private static final String FIELD_NAME = ThirdNameValidator.FIELD_NAME;
+ private static final ThirdNameValidator thirdNameValidator = new ThirdNameValidator();
+
+ @Test
+ void emptyThirdNameIsRejected() {
+ assertEquals(ErrorMessages.Required, thirdNameValidator.isValid(""));
+ }
+
+ @Test
+ void validThirdNameIsAccepted() {
+ assertNull(thirdNameValidator.isValid("Антон"));
+ }
+}