diff --git a/pom.xml b/pom.xml
index 221d9f7..d2c0902 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,6 +18,8 @@
UTF-8
${encoding}
${encoding}
+ 1.6.3
+ 0.2.0
integration
@@ -95,6 +97,11 @@
1.18.48
provided
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
com.dlsc.formsfx
formsfx-core
@@ -165,6 +172,16 @@
lombok
1.18.48
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ ${lombok-mapstruct-binding.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
-proc:full
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java b/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java
index a2c7692..83425e8 100644
--- a/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java
@@ -14,7 +14,6 @@
import com.DesktopApplicationClientJava.Api.Dto.response.ResourceDto;
import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
import com.DesktopApplicationClientJava.session.Session;
-import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.ByteArrayOutputStream;
@@ -50,9 +49,6 @@ public ApiClient(Session session) {
.build();
this.objectMapper = new ObjectMapper();
this.objectMapper.registerModule(new JavaTimeModule());
- // FIXME: temporary — backend returns firstname/lastname instead of firstName/lastName.
- // Remove once backend uses camelCase; then unknown properties should fail fast again.
- this.objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
// ======================== AUTH ========================
diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java
index ea2d3a9..2259f22 100644
--- a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java
+++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java
@@ -8,8 +8,8 @@ public record UserDto(
UUID uuid,
String email,
Role role,
- String firstName,
- String lastName,
+ String firstname,
+ String lastname,
Boolean enabled,
LocalDateTime createdAt,
LocalDateTime updatedAt) {}
diff --git a/src/main/java/com/DesktopApplicationClientJava/mapper/FileMapper.java b/src/main/java/com/DesktopApplicationClientJava/mapper/FileMapper.java
new file mode 100644
index 0000000..aefd1ce
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/mapper/FileMapper.java
@@ -0,0 +1,14 @@
+package com.DesktopApplicationClientJava.mapper;
+
+import com.DesktopApplicationClientJava.Api.Dto.response.FileDto;
+import com.DesktopApplicationClientJava.entities.FileInfo;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface FileMapper {
+
+ FileMapper INSTANCE = Mappers.getMapper(FileMapper.class);
+
+ FileInfo toEntity(FileDto dto);
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/mapper/ResourceMapper.java b/src/main/java/com/DesktopApplicationClientJava/mapper/ResourceMapper.java
new file mode 100644
index 0000000..60628ec
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/mapper/ResourceMapper.java
@@ -0,0 +1,18 @@
+package com.DesktopApplicationClientJava.mapper;
+
+import com.DesktopApplicationClientJava.Api.Dto.response.ResourceDto;
+import com.DesktopApplicationClientJava.entities.Resource;
+import org.mapstruct.Mapper;
+import org.mapstruct.NullValueMappingStrategy;
+import org.mapstruct.factory.Mappers;
+
+@Mapper(
+ uses = FileMapper.class,
+ // Бэкенд присылает null вместо пустого списка файлов.
+ nullValueIterableMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT)
+public interface ResourceMapper {
+
+ ResourceMapper INSTANCE = Mappers.getMapper(ResourceMapper.class);
+
+ Resource toEntity(ResourceDto dto);
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/mapper/UserMapper.java b/src/main/java/com/DesktopApplicationClientJava/mapper/UserMapper.java
new file mode 100644
index 0000000..fe8cd5e
--- /dev/null
+++ b/src/main/java/com/DesktopApplicationClientJava/mapper/UserMapper.java
@@ -0,0 +1,18 @@
+package com.DesktopApplicationClientJava.mapper;
+
+import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
+import com.DesktopApplicationClientJava.entities.User;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface UserMapper {
+
+ UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
+
+ // Имена в контракте бэкенда в нижнем регистре, в сущности — camelCase.
+ @Mapping(target = "firstName", source = "firstname")
+ @Mapping(target = "lastName", source = "lastname")
+ User toEntity(UserDto dto);
+}
diff --git a/src/main/java/com/DesktopApplicationClientJava/services/impl/AuthServiceImpl.java b/src/main/java/com/DesktopApplicationClientJava/services/impl/AuthServiceImpl.java
index 6538915..e6e0e24 100644
--- a/src/main/java/com/DesktopApplicationClientJava/services/impl/AuthServiceImpl.java
+++ b/src/main/java/com/DesktopApplicationClientJava/services/impl/AuthServiceImpl.java
@@ -3,9 +3,9 @@
import com.DesktopApplicationClientJava.Api.ApiClient;
import com.DesktopApplicationClientJava.Api.ApiException;
import com.DesktopApplicationClientJava.Api.Dto.request.Auth.LoginRequest;
-import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
import com.DesktopApplicationClientJava.entities.Role;
import com.DesktopApplicationClientJava.entities.User;
+import com.DesktopApplicationClientJava.mapper.UserMapper;
import com.DesktopApplicationClientJava.services.AuthService;
import com.DesktopApplicationClientJava.services.ServiceException;
import com.DesktopApplicationClientJava.session.Session;
@@ -25,8 +25,7 @@ public AuthServiceImpl(ApiClient apiClient, Session session) {
public User login(String email, String password) {
try {
apiClient.login(new LoginRequest(email, password));
- UserDto me = apiClient.getMe();
- User user = toEntity(me);
+ User user = UserMapper.INSTANCE.toEntity(apiClient.getMe());
session.setCurrentUser(user);
return user;
} catch (ApiException e) {
@@ -70,11 +69,6 @@ public boolean isModerator() {
return hasRole(Role.MODER);
}
- private User toEntity(UserDto dto) {
- return new User(
- dto.uuid(), dto.email(), dto.role(), dto.firstName(), dto.lastName(), dto.enabled());
- }
-
private static int level(Role role) {
return switch (role) {
case USER -> 0;
diff --git a/src/main/java/com/DesktopApplicationClientJava/services/impl/FileServiceImpl.java b/src/main/java/com/DesktopApplicationClientJava/services/impl/FileServiceImpl.java
index 26d037b..ef630a8 100644
--- a/src/main/java/com/DesktopApplicationClientJava/services/impl/FileServiceImpl.java
+++ b/src/main/java/com/DesktopApplicationClientJava/services/impl/FileServiceImpl.java
@@ -2,8 +2,8 @@
import com.DesktopApplicationClientJava.Api.ApiClient;
import com.DesktopApplicationClientJava.Api.ApiException;
-import com.DesktopApplicationClientJava.Api.Dto.response.FileDto;
import com.DesktopApplicationClientJava.entities.FileInfo;
+import com.DesktopApplicationClientJava.mapper.FileMapper;
import com.DesktopApplicationClientJava.services.FileService;
import com.DesktopApplicationClientJava.services.ServiceException;
import java.io.IOException;
@@ -20,8 +20,7 @@ public FileServiceImpl(ApiClient apiClient) {
public FileInfo uploadFile(UUID resourceId, Path file) {
try {
- FileDto dto = apiClient.uploadFile(resourceId, file);
- return new FileInfo(dto.uuid(), dto.name(), dto.contentType(), dto.size());
+ return FileMapper.INSTANCE.toEntity(apiClient.uploadFile(resourceId, file));
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
} catch (IOException | InterruptedException e) {
diff --git a/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java b/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java
index febaa69..9b566a7 100644
--- a/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java
+++ b/src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java
@@ -5,9 +5,8 @@
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.ResourceDto;
-import com.DesktopApplicationClientJava.entities.FileInfo;
import com.DesktopApplicationClientJava.entities.Resource;
+import com.DesktopApplicationClientJava.mapper.ResourceMapper;
import com.DesktopApplicationClientJava.services.ResourceService;
import com.DesktopApplicationClientJava.services.ServiceException;
import com.DesktopApplicationClientJava.services.filter.ResourceFilter;
@@ -29,7 +28,7 @@ public List search(ResourceFilter filter, long offset, long count) {
new ResourceFilterRequest(filter.getTitle(), filter.getDescription(), null, null);
try {
return apiClient.findResourcesByFilters(req, offset, count, "createdAt", "desc").stream()
- .map(this::toEntity)
+ .map(ResourceMapper.INSTANCE::toEntity)
.toList();
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
@@ -41,7 +40,8 @@ public List search(ResourceFilter filter, long offset, long count) {
@Override
public Resource create(String title, String description) {
try {
- return toEntity(apiClient.createResource(new CreateResourceRequest(title, description)));
+ return ResourceMapper.INSTANCE.toEntity(
+ apiClient.createResource(new CreateResourceRequest(title, description)));
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
} catch (IOException | InterruptedException e) {
@@ -52,7 +52,7 @@ public Resource create(String title, String description) {
@Override
public Resource getById(UUID resourceId) {
try {
- return toEntity(apiClient.findResourceById(resourceId));
+ return ResourceMapper.INSTANCE.toEntity(apiClient.findResourceById(resourceId));
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
} catch (IOException | InterruptedException e) {
@@ -67,7 +67,7 @@ public List search(
new ResourceFilterRequest(filter.getTitle(), filter.getDescription(), null, null);
try {
return apiClient.findResourcesByFilters(req, offset, count, sortBy, sortDir).stream()
- .map(this::toEntity)
+ .map(ResourceMapper.INSTANCE::toEntity)
.toList();
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
@@ -80,7 +80,7 @@ public List search(
public Resource update(UUID resourceId, String title, String description) {
UpdateResourceRequest req = new UpdateResourceRequest(title, description);
try {
- return toEntity(apiClient.updateResource(resourceId, req));
+ return ResourceMapper.INSTANCE.toEntity(apiClient.updateResource(resourceId, req));
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
} catch (IOException | InterruptedException e) {
@@ -98,15 +98,4 @@ public void delete(UUID resourceId) {
throw new ServiceException("Ошибка соединения с сервером");
}
}
-
- private Resource toEntity(ResourceDto dto) {
- List files =
- dto.files() == null
- ? List.of()
- : dto.files().stream()
- .map(f -> new FileInfo(f.uuid(), f.name(), f.contentType(), f.size()))
- .toList();
- return new Resource(
- dto.uuid(), dto.title(), dto.description(), dto.createdAt(), dto.updatedAt(), files);
- }
}
diff --git a/src/main/java/com/DesktopApplicationClientJava/services/impl/UsersServiceImpl.java b/src/main/java/com/DesktopApplicationClientJava/services/impl/UsersServiceImpl.java
index c4f1e7c..38c44cf 100644
--- a/src/main/java/com/DesktopApplicationClientJava/services/impl/UsersServiceImpl.java
+++ b/src/main/java/com/DesktopApplicationClientJava/services/impl/UsersServiceImpl.java
@@ -5,8 +5,8 @@
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.response.UserDto;
import com.DesktopApplicationClientJava.entities.User;
+import com.DesktopApplicationClientJava.mapper.UserMapper;
import com.DesktopApplicationClientJava.services.ServiceException;
import com.DesktopApplicationClientJava.services.UsersService;
import com.DesktopApplicationClientJava.services.data.RegisterData;
@@ -37,7 +37,7 @@ public List search(UserFilter filter, long offset, long count) {
try {
return apiClient.getUsersByFilters(req, offset, count, "createdAt", "desc").stream()
- .map(this::toEntity)
+ .map(UserMapper.INSTANCE::toEntity)
.toList();
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
@@ -49,7 +49,7 @@ public List search(UserFilter filter, long offset, long count) {
@Override
public User getById(UUID uuid) {
try {
- return toEntity(apiClient.getUserById(uuid));
+ return UserMapper.INSTANCE.toEntity(apiClient.getUserById(uuid));
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
} catch (IOException | InterruptedException e) {
@@ -67,7 +67,7 @@ public User create(RegisterData data) {
data.getFirstName(),
data.getLastName());
try {
- return toEntity(apiClient.createUser(req));
+ return UserMapper.INSTANCE.toEntity(apiClient.createUser(req));
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
} catch (IOException | InterruptedException e) {
@@ -86,7 +86,7 @@ public User update(UUID uuid, UpdateUserData data) {
data.getLastName());
try {
- return toEntity(apiClient.updateUser(uuid, req));
+ return UserMapper.INSTANCE.toEntity(apiClient.updateUser(uuid, req));
} catch (ApiException e) {
throw new ServiceException(e.getMessage(), e.getStatus());
} catch (IOException | InterruptedException e) {
@@ -104,9 +104,4 @@ public void delete(UUID uuid) {
throw new ServiceException("Ошибка соединения с сервером");
}
}
-
- private User toEntity(UserDto dto) {
- return new User(
- dto.uuid(), dto.email(), dto.role(), dto.firstName(), dto.lastName(), dto.enabled());
- }
}
diff --git a/src/test/java/mapper/FileMapperTest.java b/src/test/java/mapper/FileMapperTest.java
new file mode 100644
index 0000000..d29e3cb
--- /dev/null
+++ b/src/test/java/mapper/FileMapperTest.java
@@ -0,0 +1,25 @@
+package mapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.DesktopApplicationClientJava.Api.Dto.response.FileDto;
+import com.DesktopApplicationClientJava.entities.FileInfo;
+import com.DesktopApplicationClientJava.mapper.FileMapper;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+
+class FileMapperTest {
+
+ @Test
+ void mapsAllFields() {
+ UUID uuid = UUID.randomUUID();
+ FileDto dto = new FileDto(uuid, "отчёт.pdf", "application/pdf", 12345L);
+
+ FileInfo file = FileMapper.INSTANCE.toEntity(dto);
+
+ assertEquals(uuid, file.getUuid());
+ assertEquals("отчёт.pdf", file.getName());
+ assertEquals("application/pdf", file.getContentType());
+ assertEquals(12345L, file.getSize());
+ }
+}
diff --git a/src/test/java/mapper/ResourceMapperTest.java b/src/test/java/mapper/ResourceMapperTest.java
new file mode 100644
index 0000000..9fafa94
--- /dev/null
+++ b/src/test/java/mapper/ResourceMapperTest.java
@@ -0,0 +1,54 @@
+package mapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.DesktopApplicationClientJava.Api.Dto.response.FileDto;
+import com.DesktopApplicationClientJava.Api.Dto.response.ResourceDto;
+import com.DesktopApplicationClientJava.entities.Resource;
+import com.DesktopApplicationClientJava.mapper.ResourceMapper;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+
+class ResourceMapperTest {
+
+ @Test
+ void mapsAllFieldsIncludingFiles() {
+ UUID uuid = UUID.randomUUID();
+ UUID fileUuid = UUID.randomUUID();
+ ResourceDto dto =
+ new ResourceDto(
+ uuid,
+ "Отчёт",
+ "Описание",
+ LocalDateTime.parse("2026-01-02T03:04:05"),
+ LocalDateTime.parse("2026-01-03T04:05:06"),
+ List.of(new FileDto(fileUuid, "отчёт.pdf", "application/pdf", 12345L)));
+
+ Resource resource = ResourceMapper.INSTANCE.toEntity(dto);
+
+ assertEquals(uuid, resource.getUuid());
+ assertEquals("Отчёт", resource.getTitle());
+ assertEquals("Описание", resource.getDescription());
+ assertEquals(LocalDateTime.parse("2026-01-02T03:04:05"), resource.getCreatedAt());
+ assertEquals(LocalDateTime.parse("2026-01-03T04:05:06"), resource.getUpdatedAt());
+ assertEquals(1, resource.getFiles().size());
+ assertEquals(fileUuid, resource.getFiles().get(0).getUuid());
+ assertEquals("отчёт.pdf", resource.getFiles().get(0).getName());
+ assertEquals("application/pdf", resource.getFiles().get(0).getContentType());
+ assertEquals(12345L, resource.getFiles().get(0).getSize());
+ }
+
+ @Test
+ void nullFilesBecomeEmptyList() {
+ ResourceDto dto =
+ new ResourceDto(
+ UUID.randomUUID(), "Отчёт", "Описание", LocalDateTime.now(), LocalDateTime.now(), null);
+
+ Resource resource = ResourceMapper.INSTANCE.toEntity(dto);
+
+ assertTrue(resource.getFiles().isEmpty());
+ }
+}
diff --git a/src/test/java/mapper/UserDtoParsingTest.java b/src/test/java/mapper/UserDtoParsingTest.java
new file mode 100644
index 0000000..1de23bd
--- /dev/null
+++ b/src/test/java/mapper/UserDtoParsingTest.java
@@ -0,0 +1,44 @@
+package mapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.junit.jupiter.api.Test;
+
+class UserDtoParsingTest {
+
+ private final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
+
+ @Test
+ void parsesBackendContractOneToOne() throws Exception {
+ String json =
+ """
+ {
+ "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "email": "user@example.com",
+ "role": "USER",
+ "firstname": "Иван",
+ "lastname": "Петров",
+ "enabled": true,
+ "createdAt": "2026-01-02T03:04:05",
+ "updatedAt": "2026-01-03T04:05:06"
+ }
+ """;
+
+ UserDto dto = mapper.readValue(json, UserDto.class);
+
+ assertEquals("Иван", dto.firstname());
+ assertEquals("Петров", dto.lastname());
+ assertEquals("user@example.com", dto.email());
+ }
+
+ @Test
+ void unknownPropertiesFailFast() {
+ String json = "{\"uuid\": null, \"unexpectedField\": 1}";
+
+ assertThrows(Exception.class, () -> mapper.readValue(json, UserDto.class));
+ }
+}
diff --git a/src/test/java/mapper/UserMapperTest.java b/src/test/java/mapper/UserMapperTest.java
new file mode 100644
index 0000000..66486b8
--- /dev/null
+++ b/src/test/java/mapper/UserMapperTest.java
@@ -0,0 +1,38 @@
+package mapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
+import com.DesktopApplicationClientJava.entities.Role;
+import com.DesktopApplicationClientJava.entities.User;
+import com.DesktopApplicationClientJava.mapper.UserMapper;
+import java.time.LocalDateTime;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+
+class UserMapperTest {
+
+ @Test
+ void mapsBackendLowercaseNamesToCamelCaseFields() {
+ UUID uuid = UUID.randomUUID();
+ UserDto dto =
+ new UserDto(
+ uuid,
+ "user@example.com",
+ Role.USER,
+ "Иван",
+ "Петров",
+ true,
+ LocalDateTime.parse("2026-01-02T03:04:05"),
+ LocalDateTime.parse("2026-01-03T04:05:06"));
+
+ User user = UserMapper.INSTANCE.toEntity(dto);
+
+ assertEquals(uuid, user.getUuid());
+ assertEquals("user@example.com", user.getEmail());
+ assertEquals(Role.USER, user.getRole());
+ assertEquals("Иван", user.getFirstName());
+ assertEquals("Петров", user.getLastName());
+ assertEquals(true, user.getEnabled());
+ }
+}