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()); + } +}