From f683ac9dfcc4ce677fb9051cb2a65b51b3b57f7a Mon Sep 17 00:00:00 2001 From: Matheus Pigatto <1304matheus@gmail.com> Date: Mon, 3 Mar 2025 14:18:26 -0300 Subject: [PATCH 01/89] feat: adds new route to update folder name --- .../name/DefaultUpdateFolderNameUseCase.java | 48 +++++++++++++++++++ .../update/name/UpdateFolderNameInput.java | 9 ++++ .../update/name/UpdateFolderNameUseCase.java | 7 +++ .../drive/infrastructure/api/FolderAPI.java | 3 ++ .../api/controller/FolderController.java | 15 +++++- .../usecase/FolderUseCaseConfig.java | 7 +++ 6 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java create mode 100644 application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java create mode 100644 application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameUseCase.java diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java new file mode 100644 index 00000000..9f5d9604 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java @@ -0,0 +1,48 @@ +package com.callv2.drive.application.folder.update.name; + +import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.domain.exception.ValidationException; +import com.callv2.drive.domain.folder.Folder; +import com.callv2.drive.domain.folder.FolderGateway; +import com.callv2.drive.domain.folder.FolderID; +import com.callv2.drive.domain.folder.FolderName; +import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.handler.Notification; + +public class DefaultUpdateFolderNameUseCase extends UpdateFolderNameUseCase { + + private final FolderGateway folderGateway; + + public DefaultUpdateFolderNameUseCase(final FolderGateway folderGateway) { + this.folderGateway = folderGateway; + } + + @Override + public void execute(final UpdateFolderNameInput input) { + + final Folder folder = this.folderGateway + .findById(FolderID.of(input.folderId())) + .orElseThrow(() -> NotFoundException.with(Folder.class, input.folderId().toString())); + + final Folder parentFolder = this.folderGateway + .findById(folder.getParentFolder()) + .orElseThrow( + () -> NotFoundException.with(Folder.class, folder.getParentFolder().getValue().toString())); + + final FolderName folderName = FolderName.of(input.name()); + validateFolderName(parentFolder, folderName); + + this.folderGateway.update(folder.changeName(folderName)); + } + + private void validateFolderName(Folder parentFolder, FolderName name) { + + final Notification notification = Notification.create(); + + if (parentFolder.getSubFolders().stream().anyMatch(subFolder -> subFolder.name().equals(name))) + notification.append(Error.with("Folder with the same name already exists")); + + if (notification.hasError()) + throw ValidationException.with("Could not update folder name Aggregate Folder", notification); + } +} \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java new file mode 100644 index 00000000..9531c6d6 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java @@ -0,0 +1,9 @@ +package com.callv2.drive.application.folder.update.name; + +import java.util.UUID; + +public record UpdateFolderNameInput( + UUID folderId, + String name) { + +} diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameUseCase.java new file mode 100644 index 00000000..5b588846 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameUseCase.java @@ -0,0 +1,7 @@ +package com.callv2.drive.application.folder.update.name; + +import com.callv2.drive.application.UnitUseCase; + +public abstract class UpdateFolderNameUseCase extends UnitUseCase { + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java index 62856f1b..faa2c4c6 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java @@ -89,4 +89,7 @@ ResponseEntity> list( @RequestParam(name = "filterOperator", required = false, defaultValue = "AND") Filter.Operator filterOperator, @RequestParam(name = "filters", required = false) List filters); + @PatchMapping(value = "{id}/change-name", consumes = { MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity changeName(@PathVariable(required = true) UUID id, @RequestBody String request); + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java index f2c4e21a..73b4e19c 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java @@ -13,6 +13,8 @@ import com.callv2.drive.application.folder.retrieve.get.GetFolderUseCase; import com.callv2.drive.application.folder.retrieve.get.root.GetRootFolderUseCase; import com.callv2.drive.application.folder.retrieve.list.ListFoldersUseCase; +import com.callv2.drive.application.folder.update.name.UpdateFolderNameInput; +import com.callv2.drive.application.folder.update.name.UpdateFolderNameUseCase; import com.callv2.drive.domain.pagination.Filter; import com.callv2.drive.domain.pagination.Page; import com.callv2.drive.domain.pagination.Pagination; @@ -36,18 +38,21 @@ public class FolderController implements FolderAPI { private final GetFolderUseCase getFolderUseCase; private final MoveFolderUseCase moveFolderUseCase; private final ListFoldersUseCase listFoldersUseCase; + private final UpdateFolderNameUseCase updateFolderNameUseCase; public FolderController( final GetRootFolderUseCase getRootFolderUseCase, final CreateFolderUseCase createFolderUseCase, final GetFolderUseCase getFolderUseCase, final MoveFolderUseCase moveFolderUseCase, - final ListFoldersUseCase listFoldersUseCase) { + final ListFoldersUseCase listFoldersUseCase, + final UpdateFolderNameUseCase updateFolderNameUseCase) { this.getRootFolderUseCase = getRootFolderUseCase; this.createFolderUseCase = createFolderUseCase; this.getFolderUseCase = getFolderUseCase; this.moveFolderUseCase = moveFolderUseCase; this.listFoldersUseCase = listFoldersUseCase; + this.updateFolderNameUseCase = updateFolderNameUseCase; } @Override @@ -99,4 +104,12 @@ public ResponseEntity> list( return ResponseEntity.ok(listFoldersUseCase.execute(query).map(FolderPresenter::present)); } + @Override + public ResponseEntity changeName(UUID id, String request) { + + this.updateFolderNameUseCase.execute(new UpdateFolderNameInput(id, request)); + + return ResponseEntity.noContent().build(); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FolderUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FolderUseCaseConfig.java index 79365121..cf5c326b 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FolderUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FolderUseCaseConfig.java @@ -13,6 +13,8 @@ import com.callv2.drive.application.folder.retrieve.get.root.GetRootFolderUseCase; import com.callv2.drive.application.folder.retrieve.list.DefaultListFoldersUseCase; import com.callv2.drive.application.folder.retrieve.list.ListFoldersUseCase; +import com.callv2.drive.application.folder.update.name.DefaultUpdateFolderNameUseCase; +import com.callv2.drive.application.folder.update.name.UpdateFolderNameUseCase; import com.callv2.drive.domain.file.FileGateway; import com.callv2.drive.domain.folder.FolderGateway; @@ -54,4 +56,9 @@ ListFoldersUseCase listFoldersUseCase() { return new DefaultListFoldersUseCase(folderGateway); } + @Bean + UpdateFolderNameUseCase updateFolderNameUseCase() { + return new DefaultUpdateFolderNameUseCase(folderGateway); + } + } From 377eadc6198d9da6698ecaa6e0420aef8d57691a Mon Sep 17 00:00:00 2001 From: Artur Lampert Date: Thu, 29 May 2025 14:20:31 -0300 Subject: [PATCH 02/89] feat: delete file use case --- .../file/delete/DefaultDeleteFileUseCase.java | 50 +++++++++++++++++++ .../file/delete/DeleteFileInput.java | 10 ++++ .../file/delete/DeleteFileUseCase.java | 6 +++ .../callv2/drive/domain/file/FileGateway.java | 1 + 4 files changed, 67 insertions(+) create mode 100644 application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java create mode 100644 application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java create mode 100644 application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileUseCase.java diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java new file mode 100644 index 00000000..31cc36b7 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java @@ -0,0 +1,50 @@ +package com.callv2.drive.application.file.delete; + +import com.callv2.drive.domain.exception.InternalErrorException; +import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.domain.file.File; +import com.callv2.drive.domain.file.FileGateway; +import com.callv2.drive.domain.file.FileID; +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.storage.StorageService; + +import java.util.Objects; + +public class DefaultDeleteFileUseCase extends DeleteFileUseCase { + private final MemberGateway memberGateway; + private final FileGateway fileGateway; + private final StorageService storageService; + public DefaultDeleteFileUseCase( + final MemberGateway memberGateway, + final FileGateway fileGateway, + final StorageService storageService) { + this.memberGateway = Objects.requireNonNull(memberGateway); + this.fileGateway = Objects.requireNonNull(fileGateway); + this.storageService = Objects.requireNonNull(storageService); + } + @Override + public void execute(DeleteFileInput input) { + final MemberID ownerId = MemberID.of(input.ownerId()); + final FileID fileId = FileID.of(input.fileId()); + + memberGateway.findById(ownerId) + .orElseThrow(() -> NotFoundException.with(Member.class, input.ownerId())); + + final File file = fileGateway.findById(fileId) + .orElseThrow(() -> NotFoundException.with(File.class, input.fileId().toString())); + + fileGateway.deleteById(file.getId()); + + deleteContentFile(file.getContent().location()); + } + + private void deleteContentFile(final String contentLocation) { + try { + storageService.delete(contentLocation); + } catch (Exception e) { + throw InternalErrorException.with("Could not delete BinaryContent", e); + } + } +} diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java new file mode 100644 index 00000000..9b227560 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java @@ -0,0 +1,10 @@ +package com.callv2.drive.application.file.delete; + +import java.util.UUID; + +public record DeleteFileInput(String ownerId, UUID fileId) { + + public static DeleteFileInput of(final String ownerId, final UUID fileId) { + return new DeleteFileInput(ownerId, fileId); + } +} diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileUseCase.java new file mode 100644 index 00000000..64658391 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileUseCase.java @@ -0,0 +1,6 @@ +package com.callv2.drive.application.file.delete; + +import com.callv2.drive.application.UnitUseCase; + +public abstract class DeleteFileUseCase extends UnitUseCase { +} diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java b/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java index 11fa6118..f38b9b3d 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java @@ -20,4 +20,5 @@ public interface FileGateway { Page findAll(SearchQuery searchQuery); + void deleteById(FileID id); } From 64ac93578b0fa2eff3fe61b439d0ca1c7ef311e8 Mon Sep 17 00:00:00 2001 From: Artur Lampert Date: Mon, 2 Jun 2025 11:17:17 -0300 Subject: [PATCH 03/89] test: initial test delete file usecase --- .../delete/DefaultDeleteFileUseCaseTest.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java new file mode 100644 index 00000000..8de66908 --- /dev/null +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -0,0 +1,83 @@ +package com.callv2.drive.application.file.delete; + +import com.callv2.drive.domain.file.*; +import com.callv2.drive.domain.folder.FolderID; +import com.callv2.drive.domain.member.*; +import com.callv2.drive.domain.storage.StorageService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.Optional; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class DefaultDeleteFileUseCaseTest { + + @InjectMocks + DefaultDeleteFileUseCase useCase; + + @Mock + MemberGateway memberGateway; + + @Mock + FileGateway fileGateway; + + @Mock + StorageService storageService; + + @Test + void givenAValidParam_whenExecute_thenShouldDeleteFile() { + + final Member owner = Member.create(MemberID.of("owner")) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); + + final FileID expectedFileId = FileID.unique(); + final MemberID expectedOwnerId = owner.getId(); + final FolderID expectedFolderId = FolderID.unique(); + final FileName expectedFileName = FileName.of("file.txt"); + final Content expectedContent = Content.of( + "file.txt", + "text/plain", + 100L + ); + final Instant expectedCreatedAt = Instant.now().minus( + java.time.Duration.ofDays(1) + ); + final Instant expectedUpdatedAt = Instant.now(); + final var file = File.with( + expectedFileId, + expectedOwnerId, + expectedFolderId, + expectedFileName, + expectedContent, + expectedCreatedAt, + expectedUpdatedAt + ); + + when(memberGateway.findById(expectedOwnerId)) + .thenReturn(Optional.of(owner)); + + when(fileGateway.findById(expectedFileId)) + .thenReturn(Optional.of(file)); + + final DeleteFileInput input = DeleteFileInput.of( + expectedOwnerId.getValue(), + expectedFileId.getValue() + ); + + useCase.execute(input); + + verify(fileGateway).deleteById(expectedFileId); + verify(storageService).delete(expectedContent.location()); + + } + + +} From 30781d1530bfb4aa6f89645d80b7c1884b0957f9 Mon Sep 17 00:00:00 2001 From: Artur Lampert Date: Mon, 2 Jun 2025 11:23:18 -0300 Subject: [PATCH 04/89] chore: ignore intellij folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e8fb0dbb..dbd18e1d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ bin .vscode +.idea/ *.db From 1f99ca396cbd3b26a5c570cc1e7376f303d7ead5 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:09:40 -0300 Subject: [PATCH 05/89] feat: add RabbitMQ configuration to environment files and build.gradle Co-authored-by: Artur S --- .env.example | 5 +++++ infrastructure/build.gradle | 2 ++ infrastructure/src/main/resources/application-env.yml | 6 ++++++ infrastructure/src/main/resources/application.yml | 5 +++++ 4 files changed, 18 insertions(+) diff --git a/.env.example b/.env.example index 55bf672f..33a67342 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,11 @@ POSTGRES_PASSWORD=postgres DB_DDL_AUTO=none +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_USERNAME=admin +RABBITMQ_PASSWORD=admin + STORAGE_LOCATION=/srv/drive/storage/ MAX_FILE_SIZE=2048MB MAX_REQUEST_SIZE=100MB diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 4305c30a..04c7d912 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -22,6 +22,8 @@ dependencies { implementation(project(":application")) implementation 'org.springframework.boot:spring-boot-starter-web' + + implementation 'org.springframework.boot:spring-boot-starter-amqp' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.postgresql:postgresql:42.7.5' diff --git a/infrastructure/src/main/resources/application-env.yml b/infrastructure/src/main/resources/application-env.yml index 8d6937b4..8f40fb5f 100644 --- a/infrastructure/src/main/resources/application-env.yml +++ b/infrastructure/src/main/resources/application-env.yml @@ -26,6 +26,12 @@ db: request-timeout: ${REQUEST_TIMEOUT} +rabbitmq: + host: ${RABBITMQ_HOST} + port: ${RABBITMQ_PORT} + username: ${RABBITMQ_USERNAME} + password: ${RABBITMQ_PASSWORD} + storage: max-file-size: ${MAX_FILE_SIZE} max-request-size: ${MAX_REQUEST_SIZE} diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index 2d954a2b..73fa289a 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -34,3 +34,8 @@ spring: hibernate: ddl-auto: ${db.ddl-auto} show-sql: false + rabbitmq: + host: ${rabbitmq.host} + port: ${rabbitmq.port} + username: ${rabbitmq.username} + password: ${rabbitmq.password} \ No newline at end of file From ad63ba4826cadbb1f2376a466f7afab69403a369 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:10:47 -0300 Subject: [PATCH 06/89] feat: add RabbitMQ configuration and listener interface Co-authored-by: Artur S --- .../configuration/messaging/RabbitMQConfig.java | 16 ++++++++++++++++ .../messaging/listener/Listener.java | 8 ++++++++ 2 files changed, 24 insertions(+) create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/Listener.java diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java new file mode 100644 index 00000000..f0a31110 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java @@ -0,0 +1,16 @@ +package com.callv2.drive.infrastructure.configuration.messaging; + +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + + @Bean + MessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/Listener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/Listener.java new file mode 100644 index 00000000..a94e6de6 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/Listener.java @@ -0,0 +1,8 @@ +package com.callv2.drive.infrastructure.messaging.listener; + +@FunctionalInterface +public interface Listener { + + void handle(T data); + +} From 10f210acde4586212513db90a63ae400c7ce6e74 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:11:39 -0300 Subject: [PATCH 07/89] feat: add username and nickname fields to Member, MemberJpaEntity, and validation logic Co-authored-by: Artur S --- .../callv2/drive/domain/member/Member.java | 23 ++++++++++++-- .../drive/domain/member/MemberValidator.java | 20 +++++++++++++ .../callv2/drive/domain/member/Nickname.java | 19 ++++++++++++ .../callv2/drive/domain/member/Username.java | 22 ++++++++++++++ .../member/persistence/MemberJpaEntity.java | 30 +++++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 domain/src/main/java/com/callv2/drive/domain/member/Nickname.java create mode 100644 domain/src/main/java/com/callv2/drive/domain/member/Username.java diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index dec93581..17baa93b 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -10,6 +10,9 @@ public class Member extends AggregateRoot { + private Username username; + private Nickname nickname; + private Quota quota; private Optional quotaRequest; @@ -18,32 +21,38 @@ public class Member extends AggregateRoot { private Member( final MemberID id, + final Username username, + final Nickname nickname, final Quota quota, final QuotaRequest quotaRequest, final Instant createdAt, final Instant updatedAt) { super(id); + this.username = username; + this.nickname = nickname; this.quota = quota; this.quotaRequest = Optional.ofNullable(quotaRequest); this.createdAt = createdAt; this.updatedAt = updatedAt; } - public static Member create(final MemberID id) { + public static Member create(final MemberID id, final Nickname nickname, final Username username) { final Instant now = Instant.now(); final Quota quota = Quota.of(0, QuotaUnit.BYTE); - return new Member(id, quota, null, now, now); + return new Member(id, username, nickname, quota, null, now, now); } public static Member with( final MemberID id, + final Username username, + final Nickname nickname, final Quota quota, final QuotaRequest quotaRequest, final Instant createdAt, final Instant updatedAt) { - return new Member(id, quota, quotaRequest, createdAt, updatedAt); + return new Member(id, username, nickname, quota, quotaRequest, createdAt, updatedAt); } @Override @@ -99,6 +108,14 @@ public Member reproveQuotaRequest() { return this; } + public Username getUsername() { + return username; + } + + public Nickname getNickname() { + return nickname; + } + public Quota getQuota() { return quota; } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java b/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java index 0558a0f6..1d1805d0 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java @@ -16,6 +16,8 @@ protected MemberValidator(final Member aMember, final ValidationHandler aHandler @Override public void validate() { validateId(); + validateUsername(); + validateNickname(); validateQuota(); validateQuotaRequest(); } @@ -29,6 +31,24 @@ private void validateId() { this.member.getId().validate(this.validationHandler()); } + private void validateUsername() { + if (this.member.getUsername() == null) { + this.validationHandler().append(Error.with("'username' is required")); + return; + } + + this.member.getUsername().validate(this.validationHandler()); + } + + private void validateNickname() { + if (this.member.getNickname() == null) { + this.validationHandler().append(Error.with("'nickname' is required")); + return; + } + + this.member.getNickname().validate(this.validationHandler()); + } + private void validateQuota() { if (this.member.getQuota() == null) { this.validationHandler().append(Error.with("'quota' is required")); diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java b/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java new file mode 100644 index 00000000..aee84fc2 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java @@ -0,0 +1,19 @@ +package com.callv2.drive.domain.member; + +import com.callv2.drive.domain.ValueObject; +import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationHandler; + +public record Nickname(String value) implements ValueObject { + + public static Nickname of(final String nickname) { + return new Nickname(nickname); + } + + @Override + public void validate(final ValidationHandler aHandler) { + if (value == null || value.isBlank()) + aHandler.append(Error.with("'nickname' is required")); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Username.java b/domain/src/main/java/com/callv2/drive/domain/member/Username.java new file mode 100644 index 00000000..9f01578b --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/member/Username.java @@ -0,0 +1,22 @@ +package com.callv2.drive.domain.member; + +import com.callv2.drive.domain.ValueObject; +import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationHandler; + +public record Username(String value) implements ValueObject { + + public static Username of(final String username) { + return new Username(username); + } + + @Override + public void validate(final ValidationHandler aHandler) { + if (value == null || value.isBlank()) + aHandler.append(Error.with("'username' is required")); + + if (value.contains(" ")) + aHandler.append(Error.with("'username' cannot contain spaces")); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java index 15b72e4d..15e7a01e 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java @@ -4,9 +4,11 @@ import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; import com.callv2.drive.domain.member.Quota; import com.callv2.drive.domain.member.QuotaRequest; import com.callv2.drive.domain.member.QuotaUnit; +import com.callv2.drive.domain.member.Username; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -22,6 +24,10 @@ public class MemberJpaEntity { @Id private String id; + private String username; + + private String nickname; + @Column(nullable = false) private Long quotaAmmount; @@ -42,6 +48,8 @@ public class MemberJpaEntity { public MemberJpaEntity( final String id, + final String username, + final String nickname, final Long quotaAmmount, final QuotaUnit quotaUnit, final Long quotaRequestAmmount, @@ -50,6 +58,8 @@ public MemberJpaEntity( final Instant createdAt, final Instant updatedAt) { this.id = id; + this.username = username; + this.nickname = nickname; this.quotaAmmount = quotaAmmount; this.quotaUnit = quotaUnit; this.quotaRequestAmmount = quotaRequestAmmount; @@ -72,6 +82,8 @@ public Member toDomain() { return Member.with( MemberID.of(getId()), + Username.of(getUsername()), + Nickname.of(getNickname()), Quota.of(getQuotaAmmount(), getQuotaUnit()), quotaRequest, getCreatedAt(), @@ -81,6 +93,8 @@ public Member toDomain() { public static MemberJpaEntity fromDomain(final Member member) { return new MemberJpaEntity( member.getId().getValue(), + member.getUsername().value(), + member.getNickname().value(), member.getQuota().amount(), member.getQuota().unit(), member.getQuotaRequest().map(QuotaRequest::quota).map(Quota::amount).orElse(null), @@ -98,6 +112,22 @@ public void setId(String id) { this.id = id; } + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + public Long getQuotaAmmount() { return quotaAmmount; } From c92564937b50db0fc813b03b39eadc197b48e960 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:12:29 -0300 Subject: [PATCH 08/89] feat: implement CreateMemberInput and DefaultCreateMemberUseCase for member creation logic Co-authored-by: Artur S --- .../member/create/CreateMemberInput.java | 15 ++++++++ .../member/create/CreateMemberUseCase.java | 7 ++++ .../create/DefaultCreateMemberUseCase.java | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 application/src/main/java/com/callv2/drive/application/member/create/CreateMemberInput.java create mode 100644 application/src/main/java/com/callv2/drive/application/member/create/CreateMemberUseCase.java create mode 100644 application/src/main/java/com/callv2/drive/application/member/create/DefaultCreateMemberUseCase.java diff --git a/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberInput.java b/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberInput.java new file mode 100644 index 00000000..1a4ea74b --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberInput.java @@ -0,0 +1,15 @@ +package com.callv2.drive.application.member.create; + +public record CreateMemberInput( + String id, + String username, + String nickname) { + + public static CreateMemberInput from( + final String id, + final String username, + final String nickname) { + return new CreateMemberInput(id, username, nickname); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberUseCase.java new file mode 100644 index 00000000..25e500e0 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberUseCase.java @@ -0,0 +1,7 @@ +package com.callv2.drive.application.member.create; + +import com.callv2.drive.application.UnitUseCase; + +public abstract class CreateMemberUseCase extends UnitUseCase { + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/create/DefaultCreateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/create/DefaultCreateMemberUseCase.java new file mode 100644 index 00000000..bfeaf20f --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/create/DefaultCreateMemberUseCase.java @@ -0,0 +1,37 @@ +package com.callv2.drive.application.member.create; + +import com.callv2.drive.domain.exception.ValidationException; +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; +import com.callv2.drive.domain.member.Username; +import com.callv2.drive.domain.validation.handler.Notification; + +public class DefaultCreateMemberUseCase extends CreateMemberUseCase { + + private final MemberGateway memberGateway; + + public DefaultCreateMemberUseCase(MemberGateway memberGateway) { + this.memberGateway = memberGateway; + } + + @Override + public void execute(CreateMemberInput input) { + + final MemberID id = MemberID.of(input.id()); + final Nickname nickname = Nickname.of(input.nickname()); + final Username username = Username.of(input.username()); + + final Member member = Member.create(id, nickname, username); + + final Notification notification = Notification.create(); + member.validate(notification); + + if (notification.hasError()) + throw ValidationException.with("Validation error", notification); + + this.memberGateway.create(member); + } + +} \ No newline at end of file From fe377e44f5e0c7b6e922a3c5a9b288b7d7cc8bca Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:12:48 -0300 Subject: [PATCH 09/89] feat: replace member creation fallback with NotFoundException in DefaultCreateRequestQuotaUseCase Co-authored-by: Artur S --- .../quota/request/create/DefaultCreateRequestQuotaUseCase.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java index bd67e719..dcff72ab 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java @@ -2,6 +2,7 @@ import java.util.Objects; +import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; @@ -24,7 +25,7 @@ public void execute(final CreateRequestQuotaInput input) { final Member member = memberGateway .findById(memberId) - .orElse(memberGateway.create(Member.create(memberId))); + .orElseThrow(() -> NotFoundException.with(Member.class, input.memberId())); final Notification notification = Notification.create(); notification.valdiate(() -> member.requestQuota(Quota.of(input.ammount(), input.unit()))); From 2698506f5d53abbb2c038367c4b65007ff4e9b6d Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:12:58 -0300 Subject: [PATCH 10/89] feat: add MemberCreatedListener to handle member creation events via RabbitMQ Co-authored-by: Artur S --- .../usecase/MemberUseCaseConfig.java | 7 +++ .../member/MemberCreatedListener.java | 56 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java index 4aa4b4aa..445e5f2a 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java @@ -3,6 +3,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.callv2.drive.application.member.create.CreateMemberUseCase; +import com.callv2.drive.application.member.create.DefaultCreateMemberUseCase; import com.callv2.drive.application.member.quota.request.approve.ApproveRequestQuotaUseCase; import com.callv2.drive.application.member.quota.request.approve.DefaultApproveRequestQuotaUseCase; import com.callv2.drive.application.member.quota.request.create.CreateRequestQuotaUseCase; @@ -45,4 +47,9 @@ GetQuotaUseCase getQuotaUseCase() { return new DefaultGetQuotaUseCase(memberGateway, fileGateway); } + @Bean + CreateMemberUseCase createMemberUseCase() { + return new DefaultCreateMemberUseCase(memberGateway); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java new file mode 100644 index 00000000..f174d2eb --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java @@ -0,0 +1,56 @@ +package com.callv2.drive.infrastructure.messaging.listener.member; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Objects; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.callv2.drive.application.member.create.CreateMemberInput; +import com.callv2.drive.application.member.create.CreateMemberUseCase; +import com.callv2.drive.infrastructure.messaging.listener.Listener; + +@Component +public class MemberCreatedListener implements Listener { + + private final CreateMemberUseCase createMemberUseCase; + + public MemberCreatedListener(final CreateMemberUseCase createMemberUseCase) { + this.createMemberUseCase = Objects.requireNonNull(createMemberUseCase); + } + + @Override + @RabbitListener(queues = "drive.member.created") + public void handle(final MemberCreatedListener.Event data) { + + final Event.Data eventData = data.data(); + + final CreateMemberInput createMemberInput = CreateMemberInput.from( + eventData.id, + eventData.username, + eventData.nickname); + + createMemberUseCase.execute(createMemberInput); + } + + public record Event( + String id, + String source, + Event.Data data, + Instant occurredAt) { + + public record Data( + String id, + String username, + String email, + String nickname, + boolean isActive, + Instant createdAt, + Instant updatedAt) implements Serializable { + + } + + } + +} From c10c878b3e763b74672313c47cb58a18b37517d6 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:24:18 -0300 Subject: [PATCH 11/89] feat: add methods to change nickname and username with validation --- .../callv2/drive/domain/member/Member.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index 17baa93b..22f557a9 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -1,6 +1,7 @@ package com.callv2.drive.domain.member; import java.time.Instant; +import java.util.Objects; import java.util.Optional; import com.callv2.drive.domain.AggregateRoot; @@ -108,6 +109,28 @@ public Member reproveQuotaRequest() { return this; } + public Member changeNickname(final Nickname nickname) { + final Notification notification = Notification.create(); + Objects.requireNonNull(nickname).validate(notification); + if (notification.hasError()) + throw ValidationException.with("Change Nickname Error", notification); + + this.nickname = nickname; + this.updatedAt = Instant.now(); + return this; + } + + public Member changeUsername(final Username username) { + final Notification notification = Notification.create(); + Objects.requireNonNull(username).validate(notification); + if (notification.hasError()) + throw ValidationException.with("Change Username Error", notification); + + this.username = username; + this.updatedAt = Instant.now(); + return this; + } + public Username getUsername() { return username; } From 677fc4c8d345cd8db427e0d6238e3ad1d4881634 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:30:38 -0300 Subject: [PATCH 12/89] refactor: improve quota request validation logic in Member class --- .../java/com/callv2/drive/domain/member/Member.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index 22f557a9..1d4acaf3 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -63,21 +63,15 @@ public void validate(final ValidationHandler handler) { public Member requestQuota(final Quota quota) { - if (quota == null) - return this; - - final QuotaRequest actualQuotaRequest = this.quotaRequest.orElse(null); final QuotaRequest newQuotaRequest = QuotaRequest.of(quota, Instant.now()); - this.quotaRequest = Optional.ofNullable(newQuotaRequest); final Notification notification = Notification.create(); - this.validate(notification); + newQuotaRequest.validate(notification); - if (notification.hasError()) { - this.quotaRequest = Optional.ofNullable(actualQuotaRequest); + if (notification.hasError()) throw ValidationException.with("Request Quota Error", notification); - } + this.quotaRequest = Optional.ofNullable(newQuotaRequest); this.updatedAt = Instant.now(); return this; } From f02758a42cd60a84a6d95d6e4f54537b322a07e2 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 13:42:57 -0300 Subject: [PATCH 13/89] feat: implement DefaultUpdateMemberUseCase for updating member details Co-authored-by: Artur S --- .../update/DefaultUpdateMemberUseCase.java | 36 +++++++++++++++++++ .../member/update/UpdateMemberUseCase.java | 7 ++++ .../member/update/UpdateUserInput.java | 12 +++++++ 3 files changed, 55 insertions(+) create mode 100644 application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java create mode 100644 application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java create mode 100644 application/src/main/java/com/callv2/drive/application/member/update/UpdateUserInput.java diff --git a/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java new file mode 100644 index 00000000..57bbb998 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java @@ -0,0 +1,36 @@ +package com.callv2.drive.application.member.update; + +import java.util.Objects; + +import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.Username; +import com.callv2.drive.domain.member.Nickname; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; + +public class DefaultUpdateMemberUseCase extends UpdateMemberUseCase { + + private final MemberGateway memberGateway; + + public DefaultUpdateMemberUseCase(final MemberGateway memberGateway) { + this.memberGateway = Objects.requireNonNull(memberGateway); + } + + @Override + public void execute(final UpdateUserInput input) { + + MemberID memberId = MemberID.of(input.id()); + Username username = Username.of(input.username()); + Nickname nickname = Nickname.of(input.nickname()); + + final Member member = memberGateway.findById(memberId) + .orElseThrow(() -> NotFoundException.with(Member.class, memberId.getValue())); + + member.changeUsername(username); + member.changeNickname(nickname); + + memberGateway.update(member); + } + +} \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java new file mode 100644 index 00000000..73d6a889 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java @@ -0,0 +1,7 @@ +package com.callv2.drive.application.member.update; + +import com.callv2.drive.application.UnitUseCase; + +public abstract class UpdateMemberUseCase extends UnitUseCase { + +} \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/member/update/UpdateUserInput.java b/application/src/main/java/com/callv2/drive/application/member/update/UpdateUserInput.java new file mode 100644 index 00000000..2ef527ca --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/update/UpdateUserInput.java @@ -0,0 +1,12 @@ +package com.callv2.drive.application.member.update; + +public record UpdateUserInput( + String id, + String username, + String nickname) { + + public static UpdateUserInput from(String id, String username, String nickname) { + return new UpdateUserInput(id, username, nickname); + } + +} \ No newline at end of file From b341251ebea2d7348d7a88974c30ba2992b460ff Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 16:11:43 -0300 Subject: [PATCH 14/89] feat: implement update member use case and related event listeners --- .../update/DefaultUpdateMemberUseCase.java | 2 +- .../member/update/UpdateMemberInput.java | 12 ++++ .../member/update/UpdateMemberUseCase.java | 2 +- .../member/update/UpdateUserInput.java | 12 ---- .../exception/AlreadyExistsException.java | 37 ++++++++++++ .../callv2/drive/domain/member/Member.java | 6 ++ .../usecase/MemberUseCaseConfig.java | 7 +++ .../member/DefaultMemberGateway.java | 10 ++++ .../listener/member/MemberCreatedEvent.java | 23 ++++++++ .../member/MemberCreatedListener.java | 33 ++--------- .../listener/member/MemberUpdatedEvent.java | 24 ++++++++ .../member/MemberUpdatedListener.java | 59 +++++++++++++++++++ 12 files changed, 186 insertions(+), 41 deletions(-) create mode 100644 application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberInput.java delete mode 100644 application/src/main/java/com/callv2/drive/application/member/update/UpdateUserInput.java create mode 100644 domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedEvent.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java diff --git a/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java index 57bbb998..63587ca1 100644 --- a/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java @@ -18,7 +18,7 @@ public DefaultUpdateMemberUseCase(final MemberGateway memberGateway) { } @Override - public void execute(final UpdateUserInput input) { + public void execute(final UpdateMemberInput input) { MemberID memberId = MemberID.of(input.id()); Username username = Username.of(input.username()); diff --git a/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberInput.java b/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberInput.java new file mode 100644 index 00000000..2d86fdec --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberInput.java @@ -0,0 +1,12 @@ +package com.callv2.drive.application.member.update; + +public record UpdateMemberInput( + String id, + String username, + String nickname) { + + public static UpdateMemberInput from(String id, String username, String nickname) { + return new UpdateMemberInput(id, username, nickname); + } + +} \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java index 73d6a889..f634a094 100644 --- a/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java @@ -2,6 +2,6 @@ import com.callv2.drive.application.UnitUseCase; -public abstract class UpdateMemberUseCase extends UnitUseCase { +public abstract class UpdateMemberUseCase extends UnitUseCase { } \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/member/update/UpdateUserInput.java b/application/src/main/java/com/callv2/drive/application/member/update/UpdateUserInput.java deleted file mode 100644 index 2ef527ca..00000000 --- a/application/src/main/java/com/callv2/drive/application/member/update/UpdateUserInput.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.callv2.drive.application.member.update; - -public record UpdateUserInput( - String id, - String username, - String nickname) { - - public static UpdateUserInput from(String id, String username, String nickname) { - return new UpdateUserInput(id, username, nickname); - } - -} \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java b/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java new file mode 100644 index 00000000..73dfecd1 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java @@ -0,0 +1,37 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +import com.callv2.drive.domain.Entity; +import com.callv2.drive.domain.Identifier; +import com.callv2.drive.domain.validation.Error; + +public class AlreadyExistsException extends DomainException { + + private AlreadyExistsException(final String message) { + super(message, List.of(Error.with(message))); + } + + public static AlreadyExistsException with( + final Class>> entityClass) { + + final String message = String.format( + "%s already exists", + entityClass.getSimpleName()); + + return new AlreadyExistsException(message); + } + + public static AlreadyExistsException with( + final Class>> entityClass, + final String id) { + + final String message = String.format( + "%s with id '%s' already exists", + entityClass.getSimpleName(), + id); + + return new AlreadyExistsException(message); + } + +} \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index 1d4acaf3..60b33a21 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -109,6 +109,9 @@ public Member changeNickname(final Nickname nickname) { if (notification.hasError()) throw ValidationException.with("Change Nickname Error", notification); + if (this.nickname.equals(nickname)) + return this; + this.nickname = nickname; this.updatedAt = Instant.now(); return this; @@ -120,6 +123,9 @@ public Member changeUsername(final Username username) { if (notification.hasError()) throw ValidationException.with("Change Username Error", notification); + if (this.username.equals(username)) + return this; + this.username = username; this.updatedAt = Instant.now(); return this; diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java index 445e5f2a..5b28bd7e 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java @@ -13,6 +13,8 @@ import com.callv2.drive.application.member.quota.request.list.ListRequestQuotaUseCase; import com.callv2.drive.application.member.quota.retrieve.get.DefaultGetQuotaUseCase; import com.callv2.drive.application.member.quota.retrieve.get.GetQuotaUseCase; +import com.callv2.drive.application.member.update.DefaultUpdateMemberUseCase; +import com.callv2.drive.application.member.update.UpdateMemberUseCase; import com.callv2.drive.domain.file.FileGateway; import com.callv2.drive.domain.member.MemberGateway; @@ -52,4 +54,9 @@ CreateMemberUseCase createMemberUseCase() { return new DefaultCreateMemberUseCase(memberGateway); } + @Bean + UpdateMemberUseCase updateMemberUseCase() { + return new DefaultUpdateMemberUseCase(memberGateway); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java index 12a0000f..fdc8f163 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java @@ -4,6 +4,8 @@ import org.springframework.stereotype.Component; +import com.callv2.drive.domain.exception.AlreadyExistsException; +import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; import com.callv2.drive.domain.member.MemberID; @@ -25,6 +27,10 @@ public DefaultMemberGateway(final MemberJpaRepository memberJpaRepository) { @Override public Member create(final Member member) { + + if (this.memberJpaRepository.existsById(member.getId().getValue())) + throw AlreadyExistsException.with(Member.class, member.getId().getValue()); + return save(member); } @@ -37,6 +43,10 @@ public Optional findById(final MemberID id) { @Override public Member update(final Member member) { + + if (!this.memberJpaRepository.existsById(member.getId().getValue())) + throw NotFoundException.with(Member.class, member.getId().getValue()); + return save(member); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedEvent.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedEvent.java new file mode 100644 index 00000000..ed20c59a --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedEvent.java @@ -0,0 +1,23 @@ +package com.callv2.drive.infrastructure.messaging.listener.member; + +import java.io.Serializable; +import java.time.Instant; + +public record MemberCreatedEvent( + String id, + String source, + MemberCreatedEvent.Data data, + Instant occurredAt) implements Serializable { + + public record Data( + String id, + String username, + String email, + String nickname, + boolean isActive, + Instant createdAt, + Instant updatedAt) implements Serializable { + + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java index f174d2eb..bceb5f80 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java @@ -1,7 +1,5 @@ package com.callv2.drive.infrastructure.messaging.listener.member; -import java.io.Serializable; -import java.time.Instant; import java.util.Objects; import org.springframework.amqp.rabbit.annotation.RabbitListener; @@ -12,7 +10,7 @@ import com.callv2.drive.infrastructure.messaging.listener.Listener; @Component -public class MemberCreatedListener implements Listener { +public class MemberCreatedListener implements Listener { private final CreateMemberUseCase createMemberUseCase; @@ -22,35 +20,16 @@ public MemberCreatedListener(final CreateMemberUseCase createMemberUseCase) { @Override @RabbitListener(queues = "drive.member.created") - public void handle(final MemberCreatedListener.Event data) { + public void handle(final MemberCreatedEvent data) { - final Event.Data eventData = data.data(); + final MemberCreatedEvent.Data eventData = data.data(); final CreateMemberInput createMemberInput = CreateMemberInput.from( - eventData.id, - eventData.username, - eventData.nickname); + eventData.id(), + eventData.username(), + eventData.nickname()); createMemberUseCase.execute(createMemberInput); } - public record Event( - String id, - String source, - Event.Data data, - Instant occurredAt) { - - public record Data( - String id, - String username, - String email, - String nickname, - boolean isActive, - Instant createdAt, - Instant updatedAt) implements Serializable { - - } - - } - } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java new file mode 100644 index 00000000..9dfc9f31 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java @@ -0,0 +1,24 @@ +package com.callv2.drive.infrastructure.messaging.listener.member; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Set; + +public record MemberUpdatedEvent( + String id, + String source, + MemberUpdatedEvent.Data data, + Instant occurredAt) implements Serializable { + + public record Data( + String id, + String username, + String email, + String nickname, + boolean isActive, + Set systems, + Instant createdAt, + Instant updatedAt) implements Serializable { + + } +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java new file mode 100644 index 00000000..76589fd8 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java @@ -0,0 +1,59 @@ +package com.callv2.drive.infrastructure.messaging.listener.member; + +import java.util.Objects; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.callv2.drive.application.member.create.CreateMemberInput; +import com.callv2.drive.application.member.create.CreateMemberUseCase; +import com.callv2.drive.application.member.update.UpdateMemberInput; +import com.callv2.drive.application.member.update.UpdateMemberUseCase; +import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.infrastructure.messaging.listener.Listener; + +@Component +public class MemberUpdatedListener implements Listener { + + private final UpdateMemberUseCase updateMemberUseCase; + private final CreateMemberUseCase createMemberUseCase; + + public MemberUpdatedListener( + final UpdateMemberUseCase updateMemberUseCase, + final CreateMemberUseCase createMemberUseCase) { + this.updateMemberUseCase = Objects.requireNonNull(updateMemberUseCase); + this.createMemberUseCase = Objects.requireNonNull(createMemberUseCase); + } + + @Override + @RabbitListener(queues = "drive.member.updated") + public void handle(final MemberUpdatedEvent data) { + + final MemberUpdatedEvent.Data eventData = data.data(); + + try { + updateMember(eventData); + } catch (final NotFoundException e) { + createMember(eventData); + } + } + + private void updateMember(final MemberUpdatedEvent.Data eventData) { + final UpdateMemberInput updateMemberInput = UpdateMemberInput.from( + eventData.id(), + eventData.username(), + eventData.nickname()); + + this.updateMemberUseCase.execute(updateMemberInput); + } + + private void createMember(final MemberUpdatedEvent.Data eventData) { + final CreateMemberInput updateMemberInput = CreateMemberInput.from( + eventData.id(), + eventData.username(), + eventData.nickname()); + + this.createMemberUseCase.execute(updateMemberInput); + } + +} From 453bcbc0c7e03d3640b45b6fa98cd4e586651357 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 6 Jun 2025 19:44:22 -0300 Subject: [PATCH 15/89] feat: implement member synchronization use case and related input handling --- .../member/create/CreateMemberInput.java | 15 --- .../member/create/CreateMemberUseCase.java | 7 -- .../create/DefaultCreateMemberUseCase.java | 37 -------- .../DefaultSynchronizeMemberUseCase.java | 39 ++++++++ .../synchronize/SynchronizeMemberInput.java | 23 +++++ .../synchronize/SynchronizeMemberUseCase.java | 7 ++ .../update/DefaultUpdateMemberUseCase.java | 36 ------- .../member/update/UpdateMemberInput.java | 12 --- .../member/update/UpdateMemberUseCase.java | 7 -- .../callv2/drive/domain/member/Member.java | 93 +++++++++++++------ .../drive/domain/member/MemberGateway.java | 2 +- .../security/SecurityConfig.java | 5 +- .../usecase/MemberUseCaseConfig.java | 15 +-- .../member/DefaultMemberGateway.java | 7 +- .../listener/member/MemberCreatedEvent.java | 23 ----- .../member/MemberCreatedListener.java | 35 ------- .../listener/member/MemberUpdatedEvent.java | 6 +- .../member/MemberUpdatedListener.java | 45 +++------ 18 files changed, 157 insertions(+), 257 deletions(-) delete mode 100644 application/src/main/java/com/callv2/drive/application/member/create/CreateMemberInput.java delete mode 100644 application/src/main/java/com/callv2/drive/application/member/create/CreateMemberUseCase.java delete mode 100644 application/src/main/java/com/callv2/drive/application/member/create/DefaultCreateMemberUseCase.java create mode 100644 application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java create mode 100644 application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java create mode 100644 application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberUseCase.java delete mode 100644 application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java delete mode 100644 application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberInput.java delete mode 100644 application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedEvent.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java diff --git a/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberInput.java b/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberInput.java deleted file mode 100644 index 1a4ea74b..00000000 --- a/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberInput.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.callv2.drive.application.member.create; - -public record CreateMemberInput( - String id, - String username, - String nickname) { - - public static CreateMemberInput from( - final String id, - final String username, - final String nickname) { - return new CreateMemberInput(id, username, nickname); - } - -} diff --git a/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberUseCase.java deleted file mode 100644 index 25e500e0..00000000 --- a/application/src/main/java/com/callv2/drive/application/member/create/CreateMemberUseCase.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.callv2.drive.application.member.create; - -import com.callv2.drive.application.UnitUseCase; - -public abstract class CreateMemberUseCase extends UnitUseCase { - -} diff --git a/application/src/main/java/com/callv2/drive/application/member/create/DefaultCreateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/create/DefaultCreateMemberUseCase.java deleted file mode 100644 index bfeaf20f..00000000 --- a/application/src/main/java/com/callv2/drive/application/member/create/DefaultCreateMemberUseCase.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.callv2.drive.application.member.create; - -import com.callv2.drive.domain.exception.ValidationException; -import com.callv2.drive.domain.member.Member; -import com.callv2.drive.domain.member.MemberGateway; -import com.callv2.drive.domain.member.MemberID; -import com.callv2.drive.domain.member.Nickname; -import com.callv2.drive.domain.member.Username; -import com.callv2.drive.domain.validation.handler.Notification; - -public class DefaultCreateMemberUseCase extends CreateMemberUseCase { - - private final MemberGateway memberGateway; - - public DefaultCreateMemberUseCase(MemberGateway memberGateway) { - this.memberGateway = memberGateway; - } - - @Override - public void execute(CreateMemberInput input) { - - final MemberID id = MemberID.of(input.id()); - final Nickname nickname = Nickname.of(input.nickname()); - final Username username = Username.of(input.username()); - - final Member member = Member.create(id, nickname, username); - - final Notification notification = Notification.create(); - member.validate(notification); - - if (notification.hasError()) - throw ValidationException.with("Validation error", notification); - - this.memberGateway.create(member); - } - -} \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java new file mode 100644 index 00000000..9243cdb8 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java @@ -0,0 +1,39 @@ +package com.callv2.drive.application.member.synchronize; + +import java.util.Objects; + +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; +import com.callv2.drive.domain.member.Username; + +public class DefaultSynchronizeMemberUseCase extends SynchronizeMemberUseCase { + + private final MemberGateway memberGateway; + + public DefaultSynchronizeMemberUseCase(final MemberGateway memberGateway) { + this.memberGateway = Objects.requireNonNull(memberGateway); + } + + @Override + public void execute(final SynchronizeMemberInput input) { + + final MemberID memberId = MemberID.of(input.id()); + final Username username = Username.of(input.username()); + final Nickname nickname = Nickname.of(input.nickname()); + + final Member member = this.memberGateway + .findById(memberId) + .orElse(Member.create( + memberId, + username, + nickname, + input.createdAt(), + input.updatedAt(), + input.synchronizedVersion())); + + memberGateway.synchronize(member.synchronize(member)); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java new file mode 100644 index 00000000..2f33d191 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java @@ -0,0 +1,23 @@ +package com.callv2.drive.application.member.synchronize; + +import java.time.Instant; + +public record SynchronizeMemberInput( + String id, + String username, + String nickname, + Instant createdAt, + Instant updatedAt, + Long synchronizedVersion) { + + public static SynchronizeMemberInput from( + final String id, + final String username, + final String nickname, + final Instant createdAt, + final Instant updatedAt, + final Long synchronizedVersion) { + return new SynchronizeMemberInput(id, username, nickname, createdAt, updatedAt, synchronizedVersion); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberUseCase.java new file mode 100644 index 00000000..456b4f83 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberUseCase.java @@ -0,0 +1,7 @@ +package com.callv2.drive.application.member.synchronize; + +import com.callv2.drive.application.UnitUseCase; + +public abstract class SynchronizeMemberUseCase extends UnitUseCase { + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java deleted file mode 100644 index 63587ca1..00000000 --- a/application/src/main/java/com/callv2/drive/application/member/update/DefaultUpdateMemberUseCase.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.callv2.drive.application.member.update; - -import java.util.Objects; - -import com.callv2.drive.domain.exception.NotFoundException; -import com.callv2.drive.domain.member.Member; -import com.callv2.drive.domain.member.Username; -import com.callv2.drive.domain.member.Nickname; -import com.callv2.drive.domain.member.MemberGateway; -import com.callv2.drive.domain.member.MemberID; - -public class DefaultUpdateMemberUseCase extends UpdateMemberUseCase { - - private final MemberGateway memberGateway; - - public DefaultUpdateMemberUseCase(final MemberGateway memberGateway) { - this.memberGateway = Objects.requireNonNull(memberGateway); - } - - @Override - public void execute(final UpdateMemberInput input) { - - MemberID memberId = MemberID.of(input.id()); - Username username = Username.of(input.username()); - Nickname nickname = Nickname.of(input.nickname()); - - final Member member = memberGateway.findById(memberId) - .orElseThrow(() -> NotFoundException.with(Member.class, memberId.getValue())); - - member.changeUsername(username); - member.changeNickname(nickname); - - memberGateway.update(member); - } - -} \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberInput.java b/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberInput.java deleted file mode 100644 index 2d86fdec..00000000 --- a/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberInput.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.callv2.drive.application.member.update; - -public record UpdateMemberInput( - String id, - String username, - String nickname) { - - public static UpdateMemberInput from(String id, String username, String nickname) { - return new UpdateMemberInput(id, username, nickname); - } - -} \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java deleted file mode 100644 index f634a094..00000000 --- a/application/src/main/java/com/callv2/drive/application/member/update/UpdateMemberUseCase.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.callv2.drive.application.member.update; - -import com.callv2.drive.application.UnitUseCase; - -public abstract class UpdateMemberUseCase extends UnitUseCase { - -} \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index 60b33a21..adff4e50 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -1,11 +1,11 @@ package com.callv2.drive.domain.member; import java.time.Instant; -import java.util.Objects; import java.util.Optional; import com.callv2.drive.domain.AggregateRoot; import com.callv2.drive.domain.exception.ValidationException; +import com.callv2.drive.domain.validation.Error; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.handler.Notification; @@ -20,6 +20,8 @@ public class Member extends AggregateRoot { private Instant createdAt; private Instant updatedAt; + private Long synchronizedVersion; + private Member( final MemberID id, final Username username, @@ -27,7 +29,8 @@ private Member( final Quota quota, final QuotaRequest quotaRequest, final Instant createdAt, - final Instant updatedAt) { + final Instant updatedAt, + final Long version) { super(id); this.username = username; this.nickname = nickname; @@ -35,14 +38,21 @@ private Member( this.quotaRequest = Optional.ofNullable(quotaRequest); this.createdAt = createdAt; this.updatedAt = updatedAt; + + this.synchronizedVersion = version == null ? 0L : version; } - public static Member create(final MemberID id, final Nickname nickname, final Username username) { + public static Member create( + final MemberID id, + final Username username, + final Nickname nickname, + final Instant createdAt, + final Instant updatedAt, + final Long synchronizedVersion) { - final Instant now = Instant.now(); final Quota quota = Quota.of(0, QuotaUnit.BYTE); - return new Member(id, username, nickname, quota, null, now, now); + return new Member(id, username, nickname, quota, null, createdAt, updatedAt, synchronizedVersion); } public static Member with( @@ -52,8 +62,9 @@ public static Member with( final Quota quota, final QuotaRequest quotaRequest, final Instant createdAt, - final Instant updatedAt) { - return new Member(id, username, nickname, quota, quotaRequest, createdAt, updatedAt); + final Instant updatedAt, + final Long synchronizedVersion) { + return new Member(id, username, nickname, quota, quotaRequest, createdAt, updatedAt, synchronizedVersion); } @Override @@ -61,6 +72,26 @@ public void validate(final ValidationHandler handler) { new MemberValidator(this, handler).validate(); } + public Member synchronize(final Member member) { + + if (!this.id.equals(member.id)) + throw ValidationException + .with("Member ID mismatch", Error.with("Member ID does not match")); + + if (this.synchronizedVersion > member.synchronizedVersion) + throw ValidationException + .with("Member synchronizedVersion mismatch", Error.with("Member synchronizedVersion is outdated")); + + this.nickname = member.nickname; + this.username = member.username; + + this.createdAt = member.createdAt; + this.updatedAt = member.updatedAt; + this.synchronizedVersion = member.synchronizedVersion; + + return this; + } + public Member requestQuota(final Quota quota) { final QuotaRequest newQuotaRequest = QuotaRequest.of(quota, Instant.now()); @@ -103,33 +134,33 @@ public Member reproveQuotaRequest() { return this; } - public Member changeNickname(final Nickname nickname) { - final Notification notification = Notification.create(); - Objects.requireNonNull(nickname).validate(notification); - if (notification.hasError()) - throw ValidationException.with("Change Nickname Error", notification); + // public Member changeNickname(final Nickname nickname) { + // final Notification notification = Notification.create(); + // Objects.requireNonNull(nickname).validate(notification); + // if (notification.hasError()) + // throw ValidationException.with("Change Nickname Error", notification); - if (this.nickname.equals(nickname)) - return this; + // if (this.nickname.equals(nickname)) + // return this; - this.nickname = nickname; - this.updatedAt = Instant.now(); - return this; - } + // this.nickname = nickname; + // this.updatedAt = Instant.now(); + // return this; + // } - public Member changeUsername(final Username username) { - final Notification notification = Notification.create(); - Objects.requireNonNull(username).validate(notification); - if (notification.hasError()) - throw ValidationException.with("Change Username Error", notification); + // public Member changeUsername(final Username username) { + // final Notification notification = Notification.create(); + // Objects.requireNonNull(username).validate(notification); + // if (notification.hasError()) + // throw ValidationException.with("Change Username Error", notification); - if (this.username.equals(username)) - return this; + // if (this.username.equals(username)) + // return this; - this.username = username; - this.updatedAt = Instant.now(); - return this; - } + // this.username = username; + // this.updatedAt = Instant.now(); + // return this; + // } public Username getUsername() { return username; @@ -155,4 +186,8 @@ public Instant getUpdatedAt() { return updatedAt; } + public Long getSynchronizedVersion() { + return synchronizedVersion; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java b/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java index 394345e3..be124d1d 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java @@ -7,7 +7,7 @@ public interface MemberGateway { - Member create(Member member); + Member synchronize(Member member); Optional findById(MemberID id); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/SecurityConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/SecurityConfig.java index 6995bff3..77ffa143 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/SecurityConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/SecurityConfig.java @@ -22,7 +22,8 @@ @EnableMethodSecurity public class SecurityConfig { - private static final String ROLE_ADMIN = "ADMINISTRADOR"; + private static final String ROLE_ADMIN = "CALLV2_ADMIN"; + private static final String ROLE_MEMBER = "CALLV2_DRIVE_MEMBER"; @Bean SecurityFilterChain securityFilterChain( @@ -48,7 +49,7 @@ SecurityFilterChain securityFilterChain( .permitAll() .anyRequest() - .authenticated(); + .hasAnyRole(ROLE_MEMBER); }) .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer .jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtConverter()))) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java index 5b28bd7e..14d4ac06 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java @@ -3,8 +3,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.callv2.drive.application.member.create.CreateMemberUseCase; -import com.callv2.drive.application.member.create.DefaultCreateMemberUseCase; import com.callv2.drive.application.member.quota.request.approve.ApproveRequestQuotaUseCase; import com.callv2.drive.application.member.quota.request.approve.DefaultApproveRequestQuotaUseCase; import com.callv2.drive.application.member.quota.request.create.CreateRequestQuotaUseCase; @@ -13,8 +11,8 @@ import com.callv2.drive.application.member.quota.request.list.ListRequestQuotaUseCase; import com.callv2.drive.application.member.quota.retrieve.get.DefaultGetQuotaUseCase; import com.callv2.drive.application.member.quota.retrieve.get.GetQuotaUseCase; -import com.callv2.drive.application.member.update.DefaultUpdateMemberUseCase; -import com.callv2.drive.application.member.update.UpdateMemberUseCase; +import com.callv2.drive.application.member.synchronize.DefaultSynchronizeMemberUseCase; +import com.callv2.drive.application.member.synchronize.SynchronizeMemberUseCase; import com.callv2.drive.domain.file.FileGateway; import com.callv2.drive.domain.member.MemberGateway; @@ -50,13 +48,8 @@ GetQuotaUseCase getQuotaUseCase() { } @Bean - CreateMemberUseCase createMemberUseCase() { - return new DefaultCreateMemberUseCase(memberGateway); - } - - @Bean - UpdateMemberUseCase updateMemberUseCase() { - return new DefaultUpdateMemberUseCase(memberGateway); + SynchronizeMemberUseCase synchronizeMemberUseCase() { + return new DefaultSynchronizeMemberUseCase(memberGateway); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java index fdc8f163..4d630136 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java @@ -4,7 +4,6 @@ import org.springframework.stereotype.Component; -import com.callv2.drive.domain.exception.AlreadyExistsException; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; @@ -26,11 +25,7 @@ public DefaultMemberGateway(final MemberJpaRepository memberJpaRepository) { } @Override - public Member create(final Member member) { - - if (this.memberJpaRepository.existsById(member.getId().getValue())) - throw AlreadyExistsException.with(Member.class, member.getId().getValue()); - + public Member synchronize(final Member member) { return save(member); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedEvent.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedEvent.java deleted file mode 100644 index ed20c59a..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.callv2.drive.infrastructure.messaging.listener.member; - -import java.io.Serializable; -import java.time.Instant; - -public record MemberCreatedEvent( - String id, - String source, - MemberCreatedEvent.Data data, - Instant occurredAt) implements Serializable { - - public record Data( - String id, - String username, - String email, - String nickname, - boolean isActive, - Instant createdAt, - Instant updatedAt) implements Serializable { - - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java deleted file mode 100644 index bceb5f80..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberCreatedListener.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.callv2.drive.infrastructure.messaging.listener.member; - -import java.util.Objects; - -import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.stereotype.Component; - -import com.callv2.drive.application.member.create.CreateMemberInput; -import com.callv2.drive.application.member.create.CreateMemberUseCase; -import com.callv2.drive.infrastructure.messaging.listener.Listener; - -@Component -public class MemberCreatedListener implements Listener { - - private final CreateMemberUseCase createMemberUseCase; - - public MemberCreatedListener(final CreateMemberUseCase createMemberUseCase) { - this.createMemberUseCase = Objects.requireNonNull(createMemberUseCase); - } - - @Override - @RabbitListener(queues = "drive.member.created") - public void handle(final MemberCreatedEvent data) { - - final MemberCreatedEvent.Data eventData = data.data(); - - final CreateMemberInput createMemberInput = CreateMemberInput.from( - eventData.id(), - eventData.username(), - eventData.nickname()); - - createMemberUseCase.execute(createMemberInput); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java index 9dfc9f31..442a2889 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java @@ -2,7 +2,6 @@ import java.io.Serializable; import java.time.Instant; -import java.util.Set; public record MemberUpdatedEvent( String id, @@ -16,9 +15,10 @@ public record Data( String email, String nickname, boolean isActive, - Set systems, Instant createdAt, - Instant updatedAt) implements Serializable { + Instant updatedAt, + Long version) implements Serializable { } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java index 76589fd8..f67025cb 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java @@ -5,55 +5,34 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; -import com.callv2.drive.application.member.create.CreateMemberInput; -import com.callv2.drive.application.member.create.CreateMemberUseCase; -import com.callv2.drive.application.member.update.UpdateMemberInput; -import com.callv2.drive.application.member.update.UpdateMemberUseCase; -import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.application.member.synchronize.SynchronizeMemberInput; +import com.callv2.drive.application.member.synchronize.SynchronizeMemberUseCase; import com.callv2.drive.infrastructure.messaging.listener.Listener; @Component public class MemberUpdatedListener implements Listener { - private final UpdateMemberUseCase updateMemberUseCase; - private final CreateMemberUseCase createMemberUseCase; + private final SynchronizeMemberUseCase synchronizeMemberUseCase; - public MemberUpdatedListener( - final UpdateMemberUseCase updateMemberUseCase, - final CreateMemberUseCase createMemberUseCase) { - this.updateMemberUseCase = Objects.requireNonNull(updateMemberUseCase); - this.createMemberUseCase = Objects.requireNonNull(createMemberUseCase); + public MemberUpdatedListener(final SynchronizeMemberUseCase synchronizeMemberUseCase) { + this.synchronizeMemberUseCase = Objects.requireNonNull(synchronizeMemberUseCase); } @Override - @RabbitListener(queues = "drive.member.updated") + @RabbitListener(queues = "drive.member.created") public void handle(final MemberUpdatedEvent data) { final MemberUpdatedEvent.Data eventData = data.data(); - try { - updateMember(eventData); - } catch (final NotFoundException e) { - createMember(eventData); - } - } - - private void updateMember(final MemberUpdatedEvent.Data eventData) { - final UpdateMemberInput updateMemberInput = UpdateMemberInput.from( - eventData.id(), - eventData.username(), - eventData.nickname()); - - this.updateMemberUseCase.execute(updateMemberInput); - } - - private void createMember(final MemberUpdatedEvent.Data eventData) { - final CreateMemberInput updateMemberInput = CreateMemberInput.from( + final SynchronizeMemberInput createMemberInput = SynchronizeMemberInput.from( eventData.id(), eventData.username(), - eventData.nickname()); + eventData.nickname(), + eventData.createdAt(), + eventData.updatedAt(), + eventData.version()); - this.createMemberUseCase.execute(updateMemberInput); + synchronizeMemberUseCase.execute(createMemberInput); } } From 413da093bd1b0a39123d5cb9c608f4939d0ff6c3 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 7 Jun 2025 10:58:12 -0300 Subject: [PATCH 16/89] feat: implement member synchronization with RabbitMQ configuration and event handling -wip Co-authored-by: Artur S --- .env.example | 1 + .../messaging/RabbitMQConfig.java | 39 +++++++++++++++++++ .../messaging/RabbitMQQueueProperties.java | 38 ++++++++++++++++++ .../member/persistence/MemberJpaEntity.java | 20 ++++++++-- ...UpdatedEvent.java => MemberSyncEvent.java} | 4 +- ...dListener.java => MemberSyncListener.java} | 10 ++--- .../src/main/resources/application-env.yml | 1 + .../src/main/resources/application.yml | 10 ++++- 8 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/messaging/RabbitMQQueueProperties.java rename infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/{MemberUpdatedEvent.java => MemberSyncEvent.java} (87%) rename infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/{MemberUpdatedListener.java => MemberSyncListener.java} (74%) diff --git a/.env.example b/.env.example index 33a67342..2a2acc96 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,7 @@ POSTGRES_PASSWORD=postgres DB_DDL_AUTO=none +RABBITMQ_AUTO_CREATE=true RABBITMQ_HOST=localhost RABBITMQ_PORT=5672 RABBITMQ_USERNAME=admin diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java index f0a31110..d92b2af3 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java @@ -1,10 +1,20 @@ package com.callv2.drive.infrastructure.configuration.messaging; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.Queue; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.callv2.drive.infrastructure.configuration.properties.messaging.RabbitMQQueueProperties; + @Configuration public class RabbitMQConfig { @@ -13,4 +23,33 @@ MessageConverter jsonMessageConverter() { return new Jackson2JsonMessageConverter(); } + @Bean + @ConfigurationProperties("rabbitmq.queue.member-sync") + RabbitMQQueueProperties memberSyncProperties() { + return new RabbitMQQueueProperties(); + } + + @Configuration + static class Admin { + + @Bean + Exchange memberSyncExchange(@Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { + return new TopicExchange(props.getExchange()); + } + + @Bean + Queue memberSyncQueue(@Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { + return new Queue(props.getQueue()); + } + + @Bean + Binding videoCreatedBinding( + @Qualifier("memberSyncExchange") final DirectExchange exchange, + @Qualifier("memberSyncQueue") final Queue queue, + @Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { + return BindingBuilder.bind(queue).to(exchange).with(props.getRoutingKey()); + } + + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/messaging/RabbitMQQueueProperties.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/messaging/RabbitMQQueueProperties.java new file mode 100644 index 00000000..a05a81c0 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/messaging/RabbitMQQueueProperties.java @@ -0,0 +1,38 @@ +package com.callv2.drive.infrastructure.configuration.properties.messaging; + +public class RabbitMQQueueProperties { + + private String exchange; + + private String routingKey; + + private String queue; + + public RabbitMQQueueProperties() { + } + + public String getExchange() { + return exchange; + } + + public void setExchange(String exchange) { + this.exchange = exchange; + } + + public String getRoutingKey() { + return routingKey; + } + + public void setRoutingKey(String routingKey) { + this.routingKey = routingKey; + } + + public String getQueue() { + return queue; + } + + public void setQueue(String queue) { + this.queue = queue; + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java index 15e7a01e..c0c547b6 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java @@ -46,6 +46,8 @@ public class MemberJpaEntity { private Instant updatedAt; + private Long synchronizedVersion; + public MemberJpaEntity( final String id, final String username, @@ -56,7 +58,8 @@ public MemberJpaEntity( final QuotaUnit quotaRequestUnit, final Instant quotaRequestedAt, final Instant createdAt, - final Instant updatedAt) { + final Instant updatedAt, + final Long synchronizedVersion) { this.id = id; this.username = username; this.nickname = nickname; @@ -67,6 +70,7 @@ public MemberJpaEntity( this.quotaRequestedAt = quotaRequestedAt; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.synchronizedVersion = synchronizedVersion; } public MemberJpaEntity() { @@ -87,7 +91,8 @@ public Member toDomain() { Quota.of(getQuotaAmmount(), getQuotaUnit()), quotaRequest, getCreatedAt(), - getUpdatedAt()); + getUpdatedAt(), + getSynchronizedVersion()); } public static MemberJpaEntity fromDomain(final Member member) { @@ -101,7 +106,8 @@ public static MemberJpaEntity fromDomain(final Member member) { member.getQuotaRequest().map(QuotaRequest::quota).map(Quota::unit).orElse(null), member.getQuotaRequest().map(QuotaRequest::requesteddAt).orElse(null), member.getCreatedAt(), - member.getUpdatedAt()); + member.getUpdatedAt(), + member.getSynchronizedVersion()); } public String getId() { @@ -184,4 +190,12 @@ public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + public Long getSynchronizedVersion() { + return synchronizedVersion; + } + + public void setSynchronizedVersion(Long synchronizedVersion) { + this.synchronizedVersion = synchronizedVersion; + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java similarity index 87% rename from infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java rename to infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java index 442a2889..6afe8bbb 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedEvent.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java @@ -3,10 +3,10 @@ import java.io.Serializable; import java.time.Instant; -public record MemberUpdatedEvent( +public record MemberSyncEvent( String id, String source, - MemberUpdatedEvent.Data data, + MemberSyncEvent.Data data, Instant occurredAt) implements Serializable { public record Data( diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java similarity index 74% rename from infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java rename to infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java index f67025cb..89dedcb7 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberUpdatedListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java @@ -10,19 +10,19 @@ import com.callv2.drive.infrastructure.messaging.listener.Listener; @Component -public class MemberUpdatedListener implements Listener { +public class MemberSyncListener implements Listener { private final SynchronizeMemberUseCase synchronizeMemberUseCase; - public MemberUpdatedListener(final SynchronizeMemberUseCase synchronizeMemberUseCase) { + public MemberSyncListener(final SynchronizeMemberUseCase synchronizeMemberUseCase) { this.synchronizeMemberUseCase = Objects.requireNonNull(synchronizeMemberUseCase); } @Override - @RabbitListener(queues = "drive.member.created") - public void handle(final MemberUpdatedEvent data) { + @RabbitListener(queues = "member.member.sync.drive.queue") + public void handle(final MemberSyncEvent data) { - final MemberUpdatedEvent.Data eventData = data.data(); + final MemberSyncEvent.Data eventData = data.data(); final SynchronizeMemberInput createMemberInput = SynchronizeMemberInput.from( eventData.id(), diff --git a/infrastructure/src/main/resources/application-env.yml b/infrastructure/src/main/resources/application-env.yml index 8f40fb5f..660780e6 100644 --- a/infrastructure/src/main/resources/application-env.yml +++ b/infrastructure/src/main/resources/application-env.yml @@ -27,6 +27,7 @@ db: request-timeout: ${REQUEST_TIMEOUT} rabbitmq: + auto-create: ${RABBITMQ_AUTO_CREATE} host: ${RABBITMQ_HOST} port: ${RABBITMQ_PORT} username: ${RABBITMQ_USERNAME} diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index 73fa289a..3c372efc 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -35,7 +35,15 @@ spring: ddl-auto: ${db.ddl-auto} show-sql: false rabbitmq: + dynamic: ${rabbitmq.auto-create} host: ${rabbitmq.host} port: ${rabbitmq.port} username: ${rabbitmq.username} - password: ${rabbitmq.password} \ No newline at end of file + password: ${rabbitmq.password} + +rabbitmq: + queue: + member-sync: + exchange: member.exchange + routing-key: member.sync.drive + queue: member.member.sync.drive.queue From 82afcc7ce87c1aeda550a2eb8a38895d29ae7a12 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 7 Jun 2025 11:06:23 -0300 Subject: [PATCH 17/89] refactor: update RabbitMQ configuration to use TopicExchange and improve binding definitions Co-authored-by: Artur S --- .../configuration/messaging/RabbitMQConfig.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java index d92b2af3..63bfd2e3 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java @@ -2,7 +2,6 @@ import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.Queue; @@ -32,19 +31,19 @@ RabbitMQQueueProperties memberSyncProperties() { @Configuration static class Admin { - @Bean + @Bean("memberSyncExchange") Exchange memberSyncExchange(@Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { return new TopicExchange(props.getExchange()); } - @Bean + @Bean("memberSyncQueue") Queue memberSyncQueue(@Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { return new Queue(props.getQueue()); } - @Bean - Binding videoCreatedBinding( - @Qualifier("memberSyncExchange") final DirectExchange exchange, + @Bean("memberSyncBinding") + Binding memberSyncBinding( + @Qualifier("memberSyncExchange") final TopicExchange exchange, @Qualifier("memberSyncQueue") final Queue queue, @Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { return BindingBuilder.bind(queue).to(exchange).with(props.getRoutingKey()); From 2ab1511914d48a18363d601c0e5be1d523195591 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 7 Jun 2025 14:53:59 -0300 Subject: [PATCH 18/89] refactor: replace synchronize method with update in MemberGateway and related implementations --- .../DefaultSynchronizeMemberUseCase.java | 2 +- .../drive/domain/member/MemberGateway.java | 2 -- .../member/DefaultMemberGateway.java | 34 +++++++++++------- .../persistence/MemberJpaRepository.java | 35 +++++++++++++++++++ 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java index 9243cdb8..1fbd6f12 100644 --- a/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java @@ -33,7 +33,7 @@ public void execute(final SynchronizeMemberInput input) { input.updatedAt(), input.synchronizedVersion())); - memberGateway.synchronize(member.synchronize(member)); + memberGateway.update(member.synchronize(member)); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java b/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java index be124d1d..f27c974b 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java @@ -7,8 +7,6 @@ public interface MemberGateway { - Member synchronize(Member member); - Optional findById(MemberID id); Member update(Member member); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java index 4d630136..d3b3a0b2 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java @@ -2,7 +2,9 @@ import java.util.Optional; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.member.Member; @@ -24,11 +26,6 @@ public DefaultMemberGateway(final MemberJpaRepository memberJpaRepository) { this.memberJpaRepository = memberJpaRepository; } - @Override - public Member synchronize(final Member member) { - return save(member); - } - @Override public Optional findById(final MemberID id) { return this.memberJpaRepository @@ -36,19 +33,32 @@ public Optional findById(final MemberID id) { .map(MemberJpaEntity::toDomain); } + @Transactional @Override public Member update(final Member member) { if (!this.memberJpaRepository.existsById(member.getId().getValue())) throw NotFoundException.with(Member.class, member.getId().getValue()); - return save(member); - } - - private Member save(final Member member) { - return memberJpaRepository - .save(MemberJpaEntity.fromDomain(member)) - .toDomain(); + final MemberJpaEntity memberJpa = MemberJpaEntity.fromDomain(member); + final Integer rowsUpdated = memberJpaRepository.update( + memberJpa.getId(), + memberJpa.getUsername(), + memberJpa.getNickname(), + memberJpa.getQuotaAmmount(), + memberJpa.getQuotaUnit(), + memberJpa.getQuotaRequestAmmount(), + memberJpa.getQuotaRequestUnit(), + memberJpa.getQuotaRequestedAt(), + memberJpa.getCreatedAt(), + memberJpa.getUpdatedAt(), + memberJpa.getSynchronizedVersion()); + + if (rowsUpdated != 1) + throw new OptimisticLockingFailureException( + "Member update failed due to version conflict for id: " + member.getId().getValue()); + + return member; } @Override diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java index cd56fd07..392098e6 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java @@ -1,11 +1,16 @@ package com.callv2.drive.infrastructure.member.persistence; +import java.time.Instant; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.callv2.drive.domain.member.QuotaRequestPreview; +import com.callv2.drive.domain.member.QuotaUnit; public interface MemberJpaRepository extends JpaRepository { @@ -22,4 +27,34 @@ public interface MemberJpaRepository extends JpaRepository findAllQuotaRequests(Pageable page); + @Modifying + @Query(""" + update Member m + set + m.username = :username, + m.nickname = :nickname, + m.quotaAmmount = :quotaAmmount, + m.quotaUnit = :quotaUnit, + m.quotaRequestAmmount = :quotaRequestAmmount, + m.quotaRequestUnit = :quotaRequestUnit, + m.quotaRequestedAt = :quotaRequestedAt, + m.createdAt = :createdAt, + m.updatedAt = :updatedAt, + m.synchronizedVersion = :synchronizedVersion + where m.id = :id + and (m.synchronizedVersion is null or :synchronizedVersion >= m.synchronizedVersion) + """) + Integer update( + @Param("id") String id, + @Param("username") String username, + @Param("nickname") String nickname, + @Param("quotaAmmount") Long quotaAmmount, + @Param("quotaUnit") QuotaUnit quotaUnit, + @Param("quotaRequestAmmount") Long quotaRequestAmmount, + @Param("quotaRequestUnit") QuotaUnit quotaRequestUnit, + @Param("quotaRequestedAt") Instant quotaRequestedAt, + @Param("createdAt") Instant createdAt, + @Param("updatedAt") Instant updatedAt, + @Param("synchronizedVersion") Long synchronizedVersion); + } From e404d91328ccdfae040d70a529197f493c98a581 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 7 Jun 2025 15:27:12 -0300 Subject: [PATCH 19/89] refactor: update RabbitMQ configuration properties to use messaging namespace --- .../configuration/messaging/RabbitMQConfig.java | 2 +- infrastructure/src/main/resources/application.yml | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java index 63bfd2e3..ea2c5092 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java @@ -23,7 +23,7 @@ MessageConverter jsonMessageConverter() { } @Bean - @ConfigurationProperties("rabbitmq.queue.member-sync") + @ConfigurationProperties("messaging.rabbitmq.queue.member-sync") RabbitMQQueueProperties memberSyncProperties() { return new RabbitMQQueueProperties(); } diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index 3c372efc..49a86ef4 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -41,9 +41,10 @@ spring: username: ${rabbitmq.username} password: ${rabbitmq.password} -rabbitmq: - queue: - member-sync: - exchange: member.exchange - routing-key: member.sync.drive - queue: member.member.sync.drive.queue +messaging: + rabbitmq: + queue: + member-sync: + exchange: member.exchange + routing-key: member.sync.drive + queue: member.member.sync.drive.queue From ffee54978015a6afc7ee49d8666ddba21874434d Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 7 Jun 2025 21:22:14 -0300 Subject: [PATCH 20/89] refactor: update RabbitMQ configuration and remove unused properties --- .env.example | 1 + .../messaging/RabbitMQConfig.java | 42 +++++++++---------- .../messaging/RabbitMQQueueProperties.java | 38 ----------------- .../listener/member/MemberSyncListener.java | 2 +- .../src/main/resources/application-env.yml | 1 + .../src/main/resources/application.yml | 11 +---- 6 files changed, 24 insertions(+), 71 deletions(-) delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/messaging/RabbitMQQueueProperties.java diff --git a/.env.example b/.env.example index 2a2acc96..c6608b31 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,7 @@ POSTGRES_PASSWORD=postgres DB_DDL_AUTO=none +RABBITMQ_VIRTUAL_HOST=/ RABBITMQ_AUTO_CREATE=true RABBITMQ_HOST=localhost RABBITMQ_PORT=5672 diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java index ea2c5092..ce30670e 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java @@ -2,18 +2,14 @@ import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.TopicExchange; -import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.callv2.drive.infrastructure.configuration.properties.messaging.RabbitMQQueueProperties; - @Configuration public class RabbitMQConfig { @@ -22,31 +18,31 @@ MessageConverter jsonMessageConverter() { return new Jackson2JsonMessageConverter(); } - @Bean - @ConfigurationProperties("messaging.rabbitmq.queue.member-sync") - RabbitMQQueueProperties memberSyncProperties() { - return new RabbitMQQueueProperties(); - } - @Configuration static class Admin { - @Bean("memberSyncExchange") - Exchange memberSyncExchange(@Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { - return new TopicExchange(props.getExchange()); + @Bean + TopicExchange memberExchange() { + return new TopicExchange("member.exchange"); + } + + @Bean + Queue memberSyncQueue() { + return new Queue("member.sync.drive.queue"); } - @Bean("memberSyncQueue") - Queue memberSyncQueue(@Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { - return new Queue(props.getQueue()); + @Bean + Binding memberCreatedSyncBindings( + @Qualifier("memberExchange") final TopicExchange exchange, + @Qualifier("memberSyncQueue") final Queue queue) { + return BindingBuilder.bind(queue).to(exchange).with("member.created"); } - @Bean("memberSyncBinding") - Binding memberSyncBinding( - @Qualifier("memberSyncExchange") final TopicExchange exchange, - @Qualifier("memberSyncQueue") final Queue queue, - @Qualifier("memberSyncProperties") final RabbitMQQueueProperties props) { - return BindingBuilder.bind(queue).to(exchange).with(props.getRoutingKey()); + @Bean + Binding memberUpdatedSyncBindings( + @Qualifier("memberExchange") final TopicExchange exchange, + @Qualifier("memberSyncQueue") final Queue queue) { + return BindingBuilder.bind(queue).to(exchange).with("member.updated"); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/messaging/RabbitMQQueueProperties.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/messaging/RabbitMQQueueProperties.java deleted file mode 100644 index a05a81c0..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/messaging/RabbitMQQueueProperties.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.callv2.drive.infrastructure.configuration.properties.messaging; - -public class RabbitMQQueueProperties { - - private String exchange; - - private String routingKey; - - private String queue; - - public RabbitMQQueueProperties() { - } - - public String getExchange() { - return exchange; - } - - public void setExchange(String exchange) { - this.exchange = exchange; - } - - public String getRoutingKey() { - return routingKey; - } - - public void setRoutingKey(String routingKey) { - this.routingKey = routingKey; - } - - public String getQueue() { - return queue; - } - - public void setQueue(String queue) { - this.queue = queue; - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java index 89dedcb7..c24b0227 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java @@ -19,7 +19,7 @@ public MemberSyncListener(final SynchronizeMemberUseCase synchronizeMemberUseCas } @Override - @RabbitListener(queues = "member.member.sync.drive.queue") + @RabbitListener(queues = "member.sync.drive.queue") public void handle(final MemberSyncEvent data) { final MemberSyncEvent.Data eventData = data.data(); diff --git a/infrastructure/src/main/resources/application-env.yml b/infrastructure/src/main/resources/application-env.yml index 660780e6..372edb20 100644 --- a/infrastructure/src/main/resources/application-env.yml +++ b/infrastructure/src/main/resources/application-env.yml @@ -27,6 +27,7 @@ db: request-timeout: ${REQUEST_TIMEOUT} rabbitmq: + virtual-host: ${RABBITMQ_VIRTUAL_HOST:/} auto-create: ${RABBITMQ_AUTO_CREATE} host: ${RABBITMQ_HOST} port: ${RABBITMQ_PORT} diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index 49a86ef4..9061e7fa 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -35,16 +35,9 @@ spring: ddl-auto: ${db.ddl-auto} show-sql: false rabbitmq: + virtual-host: ${rabbitmq.virtual-host} dynamic: ${rabbitmq.auto-create} host: ${rabbitmq.host} port: ${rabbitmq.port} username: ${rabbitmq.username} - password: ${rabbitmq.password} - -messaging: - rabbitmq: - queue: - member-sync: - exchange: member.exchange - routing-key: member.sync.drive - queue: member.member.sync.drive.queue + password: ${rabbitmq.password} \ No newline at end of file From d164396f3cf916b63715596d023cb72556ad2bbc Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 7 Jun 2025 22:36:18 -0300 Subject: [PATCH 21/89] refactor: streamline member synchronization logic and update related classes --- .../DefaultSynchronizeMemberUseCase.java | 23 ++++++++++++------- .../callv2/drive/domain/member/Member.java | 15 +----------- .../drive/domain/member/MemberGateway.java | 2 ++ .../AbstractMethodInvocationContext.java | 2 +- .../context/MethodInvocationContext.java | 4 ++-- .../context/PostInvocationContext.java | 2 +- ...leMethodInterceptorWithContextHandler.java | 3 ++- .../member/DefaultMemberGateway.java | 5 ++++ .../listener/member/MemberSyncEvent.java | 2 +- .../listener/member/MemberSyncListener.java | 2 +- 10 files changed, 31 insertions(+), 29 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java index 1fbd6f12..e5191924 100644 --- a/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java @@ -23,16 +23,23 @@ public void execute(final SynchronizeMemberInput input) { final Username username = Username.of(input.username()); final Nickname nickname = Nickname.of(input.nickname()); - final Member member = this.memberGateway + final var updatedMember = Member.with( + memberId, + username, + nickname, + null, + null, + input.createdAt(), + input.updatedAt(), + input.synchronizedVersion()); + + this.memberGateway .findById(memberId) - .orElse(Member.create( - memberId, - username, - nickname, - input.createdAt(), - input.updatedAt(), - input.synchronizedVersion())); + .map(member -> member.synchronize(updatedMember)) + .ifPresentOrElse(this::update, () -> memberGateway.create(updatedMember)); + } + private void update(final Member member) { memberGateway.update(member.synchronize(member)); } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index adff4e50..fbdff0a7 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -34,7 +34,7 @@ private Member( super(id); this.username = username; this.nickname = nickname; - this.quota = quota; + this.quota = quota == null ? Quota.of(0, QuotaUnit.BYTE) : quota; this.quotaRequest = Optional.ofNullable(quotaRequest); this.createdAt = createdAt; this.updatedAt = updatedAt; @@ -42,19 +42,6 @@ private Member( this.synchronizedVersion = version == null ? 0L : version; } - public static Member create( - final MemberID id, - final Username username, - final Nickname nickname, - final Instant createdAt, - final Instant updatedAt, - final Long synchronizedVersion) { - - final Quota quota = Quota.of(0, QuotaUnit.BYTE); - - return new Member(id, username, nickname, quota, null, createdAt, updatedAt, synchronizedVersion); - } - public static Member with( final MemberID id, final Username username, diff --git a/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java b/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java index f27c974b..394345e3 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/MemberGateway.java @@ -7,6 +7,8 @@ public interface MemberGateway { + Member create(Member member); + Optional findById(MemberID id); Member update(Member member); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java index 1ba24ab1..325ab18f 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java @@ -2,7 +2,7 @@ import java.time.Instant; -import javax.annotation.Nonnull; +import jakarta.annotation.Nonnull; public abstract class AbstractMethodInvocationContext implements MethodInvocationContext { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java index 151fb732..0a5b46a6 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java @@ -2,10 +2,10 @@ import java.time.Instant; -import javax.annotation.Nonnull; - import org.aopalliance.intercept.MethodInvocation; +import jakarta.annotation.Nonnull; + public interface MethodInvocationContext extends MethodInvocation { @Nonnull diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java index 41a206b8..125b22de 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java @@ -2,7 +2,7 @@ import java.time.Instant; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; public interface PostInvocationContext extends MethodInvocationContext { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java index 4ae41e75..9c000fc3 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java @@ -1,7 +1,6 @@ package com.callv2.drive.infrastructure.aop.aspects.handler; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -12,6 +11,8 @@ import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; import com.callv2.drive.infrastructure.aop.aspects.context.SimpleMethodInvocationContext; +import jakarta.annotation.Nullable; + public final class SimpleMethodInterceptorWithContextHandler implements MethodInterceptor { private final MethodInvocationAspectExecutorChain beforeChain; diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java index d3b3a0b2..408bd28e 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java @@ -26,6 +26,11 @@ public DefaultMemberGateway(final MemberJpaRepository memberJpaRepository) { this.memberJpaRepository = memberJpaRepository; } + @Override + public Member create(final Member member) { + return this.memberJpaRepository.save(MemberJpaEntity.fromDomain(member)).toDomain(); + } + @Override public Optional findById(final MemberID id) { return this.memberJpaRepository diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java index 6afe8bbb..3c17f289 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java @@ -17,7 +17,7 @@ public record Data( boolean isActive, Instant createdAt, Instant updatedAt, - Long version) implements Serializable { + Long synchronizedVersion) implements Serializable { } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java index c24b0227..0c551df7 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java @@ -30,7 +30,7 @@ public void handle(final MemberSyncEvent data) { eventData.nickname(), eventData.createdAt(), eventData.updatedAt(), - eventData.version()); + eventData.synchronizedVersion()); synchronizeMemberUseCase.execute(createMemberInput); } From 63c1773030991a6cf84fef43722de4e3d54fb9eb Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 7 Jun 2025 22:40:16 -0300 Subject: [PATCH 22/89] refactor: update Member creation in tests to use new constructor with additional parameters --- .../create/DefaultCreateFileUseCaseTest.java | 93 +++++++++++++++++-- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java index 52eab0f6..9dbe7950 100644 --- a/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java @@ -14,6 +14,7 @@ import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; +import java.time.Instant; import java.util.List; import java.util.Optional; @@ -37,8 +38,10 @@ import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; import com.callv2.drive.domain.member.Quota; import com.callv2.drive.domain.member.QuotaUnit; +import com.callv2.drive.domain.member.Username; import com.callv2.drive.domain.storage.StorageService; @ExtendWith(MockitoExtension.class) @@ -62,7 +65,15 @@ public class DefaultCreateFileUseCaseTest { @Test void givenAValidParams_whenCallsExecute_thenShouldCreateFile() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -131,7 +142,15 @@ void givenAValidParams_whenCallsExecute_thenShouldCreateFile() { @Test void givenAnInvalidFolderId_whenCallsExecute_thenShouldThrowNotFoundException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -224,7 +243,15 @@ void givenAnInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() @Test void givenAValidParamsWithAlreadyExistingFileNameOnSameFolder_whenCallsExecute_thenShouldThrowValidationException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -282,7 +309,15 @@ void givenAValidParamsWithAlreadyExistingFileNameOnSameFolder_whenCallsExecute_t @Test void givenAValidParams_whenCallsExecuteAndFileGatewayCreateThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -358,7 +393,15 @@ void givenAValidParams_whenCallsExecuteAndFileGatewayCreateThrowsRandomException @Test void givenAValidParams_whenCallsExecuteAndFileGatewayCreateAndContentGatewayDeleteThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -434,7 +477,15 @@ void givenAValidParams_whenCallsExecuteAndFileGatewayCreateAndContentGatewayDele @Test void givenAValidParams_whenCallsExecuteAndContentGatewayStoreThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -488,7 +539,15 @@ void givenAValidParams_whenCallsExecuteAndContentGatewayStoreThrowsRandomExcepti @Test void givenAnInvalidFileName_whenCallsExecute_thenShouldThrowValidationException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -540,7 +599,15 @@ void givenAnInvalidFileName_whenCallsExecute_thenShouldThrowValidationException( @Test void givenAValidParams_whenCallsExecuteAndMemberQuotaIsExceeded_thenShouldThrowsQuotaExceededException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.BYTE)) .approveQuotaRequest(); @@ -588,7 +655,15 @@ void givenAValidParams_whenCallsExecuteAndMemberQuotaIsExceeded_thenShouldThrows @Test void givenAnInvalidParamsWithContentTypeNull_whenCallsExecute_thenShouldThrowsValidationException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); From 45136374347e8f47ca6fe4efff0e6b2d5f678e15 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 7 Jun 2025 22:42:10 -0300 Subject: [PATCH 23/89] chore: update Spring Boot version to 3.4.6 in build.gradle --- infrastructure/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 04c7d912..5fadb191 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' id 'application' - id 'org.springframework.boot' version '3.4.5' + id 'org.springframework.boot' version '3.4.6' id 'io.spring.dependency-management' version '1.1.7' } From fc02dab258be4b2b87f615ceec3318d79d198dc6 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 00:39:09 -0300 Subject: [PATCH 24/89] -wip --- .../domain/exception/IdMismatchException.java | 17 ++++++++++++ .../SynchronizedVersionOutdatedException.java | 21 +++++++++++++++ .../callv2/drive/domain/member/Member.java | 12 ++++++--- .../messaging/RabbitMQConfig.java | 26 ++++++++++++++++++- .../listener/member/MemberSyncListener.java | 11 +++++++- .../src/main/resources/application.yml | 13 +++++++++- 6 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java create mode 100644 domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java b/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java new file mode 100644 index 00000000..dd7ff28e --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java @@ -0,0 +1,17 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +import com.callv2.drive.domain.validation.Error; + +public class IdMismatchException extends DomainException { + + private IdMismatchException(final String aMessage, final List errors) { + super(aMessage, errors); + } + + public static IdMismatchException with(final String aMessage, final List errors) { + return new IdMismatchException(aMessage, errors); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java b/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java new file mode 100644 index 00000000..1af1a077 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java @@ -0,0 +1,21 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +import com.callv2.drive.domain.validation.Error; + +public class SynchronizedVersionOutdatedException extends DomainException { + + private SynchronizedVersionOutdatedException(final String aMessage, final List anErrors) { + super(aMessage, anErrors); + } + + public static SynchronizedVersionOutdatedException with(final String aMessage) { + return new SynchronizedVersionOutdatedException(aMessage, List.of()); + } + + public static SynchronizedVersionOutdatedException with(final String aMessage, final List anErrors) { + return new SynchronizedVersionOutdatedException(aMessage, anErrors); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index fbdff0a7..d243b4fe 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -1,9 +1,12 @@ package com.callv2.drive.domain.member; import java.time.Instant; +import java.util.List; import java.util.Optional; import com.callv2.drive.domain.AggregateRoot; +import com.callv2.drive.domain.exception.IdMismatchException; +import com.callv2.drive.domain.exception.SynchronizedVersionOutdatedException; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.validation.Error; import com.callv2.drive.domain.validation.ValidationHandler; @@ -62,12 +65,13 @@ public void validate(final ValidationHandler handler) { public Member synchronize(final Member member) { if (!this.id.equals(member.id)) - throw ValidationException - .with("Member ID mismatch", Error.with("Member ID does not match")); + throw IdMismatchException + .with("Member ID mismatch", List.of(Error.with("Member ID does not match"))); if (this.synchronizedVersion > member.synchronizedVersion) - throw ValidationException - .with("Member synchronizedVersion mismatch", Error.with("Member synchronizedVersion is outdated")); + throw SynchronizedVersionOutdatedException + .with("Member synchronizedVersion mismatch", + List.of(Error.with("Member synchronizedVersion is outdated"))); this.nickname = member.nickname; this.username = member.username; diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java index ce30670e..3825a6f2 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java @@ -3,6 +3,7 @@ import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; @@ -26,9 +27,25 @@ TopicExchange memberExchange() { return new TopicExchange("member.exchange"); } + @Bean + TopicExchange memberDlxExchange() { + return new TopicExchange("member.dlx.exchange"); + } + @Bean Queue memberSyncQueue() { - return new Queue("member.sync.drive.queue"); + return QueueBuilder + .durable("member.sync.drive.queue") + .deadLetterExchange("member.dlx.exchange") + .deadLetterRoutingKey("member.sync.deadletter") + .build(); + } + + @Bean + Queue memberSyncDeadLetterQueue() { + return QueueBuilder + .durable("member.sync.drive.queue.deadletter") + .build(); } @Bean @@ -45,6 +62,13 @@ Binding memberUpdatedSyncBindings( return BindingBuilder.bind(queue).to(exchange).with("member.updated"); } + @Bean + Binding memberSyncDeadLetterBinding( + @Qualifier("memberDlxExchange") final TopicExchange exchange, + @Qualifier("memberSyncDeadLetterQueue") final Queue queue) { + return BindingBuilder.bind(queue).to(exchange).with("member.sync.deadletter"); + } + } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java index 0c551df7..4fb1ff00 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java @@ -3,10 +3,13 @@ import java.util.Objects; import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.stereotype.Component; import com.callv2.drive.application.member.synchronize.SynchronizeMemberInput; import com.callv2.drive.application.member.synchronize.SynchronizeMemberUseCase; +import com.callv2.drive.domain.exception.SynchronizedVersionOutdatedException; +import com.callv2.drive.domain.exception.IdMismatchException; import com.callv2.drive.infrastructure.messaging.listener.Listener; @Component @@ -32,7 +35,13 @@ public void handle(final MemberSyncEvent data) { eventData.updatedAt(), eventData.synchronizedVersion()); - synchronizeMemberUseCase.execute(createMemberInput); + try { + synchronizeMemberUseCase.execute(createMemberInput); + } catch (OptimisticLockingFailureException | SynchronizedVersionOutdatedException | IdMismatchException e) { + // TODO log WARN + System.err.println("Error synchronizing member: " + e.getMessage()); + } + } } diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index 9061e7fa..bd434fe7 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -32,9 +32,20 @@ spring: url: jdbc:postgresql://${postgres.host}:${postgres.port}/${postgres.database} jpa: hibernate: - ddl-auto: ${db.ddl-auto} + ddl-auto: none show-sql: false rabbitmq: + listener: + simple: + concurrency: 1 + retry: + max-attempts: 20 + stateless: true + enabled: true + initial-interval: 1000 + max-interval: 15000 + multiplier: 1.5 + default-requeue-rejected: false virtual-host: ${rabbitmq.virtual-host} dynamic: ${rabbitmq.auto-create} host: ${rabbitmq.host} From 9c1dcbc5b31297c7a6ba35ad78c1b488c6bf9c72 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 10:10:54 -0300 Subject: [PATCH 25/89] refactor: remove unused changeNickname and changeUsername methods from Member class --- .../callv2/drive/domain/member/Member.java | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index d243b4fe..924d1da1 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -125,34 +125,6 @@ public Member reproveQuotaRequest() { return this; } - // public Member changeNickname(final Nickname nickname) { - // final Notification notification = Notification.create(); - // Objects.requireNonNull(nickname).validate(notification); - // if (notification.hasError()) - // throw ValidationException.with("Change Nickname Error", notification); - - // if (this.nickname.equals(nickname)) - // return this; - - // this.nickname = nickname; - // this.updatedAt = Instant.now(); - // return this; - // } - - // public Member changeUsername(final Username username) { - // final Notification notification = Notification.create(); - // Objects.requireNonNull(username).validate(notification); - // if (notification.hasError()) - // throw ValidationException.with("Change Username Error", notification); - - // if (this.username.equals(username)) - // return this; - - // this.username = username; - // this.updatedAt = Instant.now(); - // return this; - // } - public Username getUsername() { return username; } From 3cf2440faa95121a7defa2ef718ce6e151e330fc Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 10:41:45 -0300 Subject: [PATCH 26/89] refactor: replace System.err logging with Log4j in MemberSyncListener --- .../messaging/listener/member/MemberSyncListener.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java index 4fb1ff00..18b45dba 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java @@ -2,19 +2,23 @@ import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.stereotype.Component; import com.callv2.drive.application.member.synchronize.SynchronizeMemberInput; import com.callv2.drive.application.member.synchronize.SynchronizeMemberUseCase; -import com.callv2.drive.domain.exception.SynchronizedVersionOutdatedException; import com.callv2.drive.domain.exception.IdMismatchException; +import com.callv2.drive.domain.exception.SynchronizedVersionOutdatedException; import com.callv2.drive.infrastructure.messaging.listener.Listener; @Component public class MemberSyncListener implements Listener { + private static final Logger log = LogManager.getLogger(MemberSyncListener.class); + private final SynchronizeMemberUseCase synchronizeMemberUseCase; public MemberSyncListener(final SynchronizeMemberUseCase synchronizeMemberUseCase) { @@ -38,8 +42,7 @@ public void handle(final MemberSyncEvent data) { try { synchronizeMemberUseCase.execute(createMemberInput); } catch (OptimisticLockingFailureException | SynchronizedVersionOutdatedException | IdMismatchException e) { - // TODO log WARN - System.err.println("Error synchronizing member: " + e.getMessage()); + log.warn("Error synchronizing member: {}", e.getMessage(), e); } } From 8d0c6f725620bdca2165a3fbbd3c354de8c83f55 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 10:41:55 -0300 Subject: [PATCH 27/89] chore: update .gitignore to include log.json.gz and log.gz patterns --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2868c3c3..013ff5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ bin .env *.log -*log.json \ No newline at end of file +*log.json + +*.log.gz +*log.json.gz \ No newline at end of file From b93ef679a6cad4b49f7f73aaa3eca77c59eeaec7 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 10:43:37 -0300 Subject: [PATCH 28/89] refactor: adjust RabbitMQ listener retry configuration for improved performance --- infrastructure/src/main/resources/application.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index bd434fe7..e2867587 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -38,13 +38,13 @@ spring: listener: simple: concurrency: 1 + max-concurrency: 20 retry: - max-attempts: 20 - stateless: true + max-attempts: 3 enabled: true initial-interval: 1000 - max-interval: 15000 - multiplier: 1.5 + max-interval: 8000 + multiplier: 2 default-requeue-rejected: false virtual-host: ${rabbitmq.virtual-host} dynamic: ${rabbitmq.auto-create} From 9e8f7aba1689e6d390c2a76b3d926ef84e7fd4d6 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 13:28:23 -0300 Subject: [PATCH 29/89] refactor: update project name and include syntax in settings.gradle --- settings.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/settings.gradle b/settings.gradle index 077578b8..6ea23965 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,2 @@ -rootProject.name = 'drive' -include 'domain' -include 'application' -include 'infrastructure' \ No newline at end of file +rootProject.name = 'drive-api' +include('domain', 'application', 'infrastructure') \ No newline at end of file From 89cbab14204406fa85cd10cee2e3c529ae907257 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 13:47:59 -0300 Subject: [PATCH 30/89] test: add unit tests for DefaultSynchronizeMemberUseCase --- .../DefaultSynchronizeMemberUseCaseTest.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java diff --git a/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java new file mode 100644 index 00000000..93757e69 --- /dev/null +++ b/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java @@ -0,0 +1,146 @@ +package com.callv2.drive.application.member.synchronize; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; +import com.callv2.drive.domain.member.Quota; +import com.callv2.drive.domain.member.QuotaUnit; +import com.callv2.drive.domain.member.Username; + +@ExtendWith(MockitoExtension.class) +public class DefaultSynchronizeMemberUseCaseTest { + + @InjectMocks + DefaultSynchronizeMemberUseCase useCase; + + @Mock + MemberGateway memberGateway; + + @Test + void givenAnValidInput_whenCallsExecute_thenShouldSynchonizeMember() { + + final var expectedIdVAlue = "123"; + final var expectedUsernameValue = "username"; + final var expectedNicknameValue = "nickname"; + final var expectedCreatedAt = java.time.Instant.now(); + final var expectedUpdatedAt = expectedCreatedAt.plusSeconds(100); + final var expectedSynchronizedVersion = 1L; + + final var expectedMemberId = MemberID.of(expectedIdVAlue); + final var expectedUsername = Username.of(expectedUsernameValue); + final var expectedNickname = Nickname.of(expectedNicknameValue); + final var expectedQuota = Quota.of(10L, QuotaUnit.GIGABYTE); + + final var oldMember = Member.with( + expectedMemberId, + expectedUsername, + expectedNickname, + expectedQuota, + null, + expectedCreatedAt, + expectedCreatedAt, + 0L); + + when(memberGateway.findById(expectedMemberId)) + .thenReturn(Optional.of(oldMember)); + + when(memberGateway.update(any())) + .thenAnswer(returnsFirstArg()); + + final var input = SynchronizeMemberInput.from( + expectedIdVAlue, + expectedUsernameValue, + expectedNicknameValue, + expectedCreatedAt, + expectedUpdatedAt, + expectedSynchronizedVersion); + + assertDoesNotThrow(() -> useCase.execute(input)); + + verify(memberGateway, times(1)).findById(eq(expectedMemberId)); + verify(memberGateway, times(1)).findById(any()); + + verify(memberGateway).update(argThat(member -> { + assertEquals(expectedMemberId, member.getId()); + assertEquals(expectedUsername, member.getUsername()); + assertEquals(expectedNickname, member.getNickname()); + assertEquals(expectedQuota, member.getQuota()); + assertEquals(expectedCreatedAt, member.getCreatedAt()); + assertEquals(expectedUpdatedAt, member.getUpdatedAt()); + assertEquals(expectedSynchronizedVersion, member.getSynchronizedVersion()); + return true; + })); + verify(memberGateway, times(1)).update(any()); + + verify(memberGateway, times(0)).create(any()); + } + + @Test + void givenAnValidInputWhitNonExistentMember_whenCallsExecute_thenShouldCreateMember() { + + final var expectedIdVAlue = "123"; + final var expectedUsernameValue = "username"; + final var expectedNicknameValue = "nickname"; + final var expectedCreatedAt = java.time.Instant.now(); + final var expectedUpdatedAt = expectedCreatedAt.plusSeconds(100); + final var expectedSynchronizedVersion = 1L; + + final var expectedMemberId = MemberID.of(expectedIdVAlue); + final var expectedUsername = Username.of(expectedUsernameValue); + final var expectedNickname = Nickname.of(expectedNicknameValue); + final var expectedQuota = Quota.of(0L, QuotaUnit.BYTE); + + when(memberGateway.findById(expectedMemberId)) + .thenReturn(Optional.empty()); + + when(memberGateway.create(any())) + .thenAnswer(returnsFirstArg()); + + final var input = SynchronizeMemberInput.from( + expectedIdVAlue, + expectedUsernameValue, + expectedNicknameValue, + expectedCreatedAt, + expectedUpdatedAt, + expectedSynchronizedVersion); + + assertDoesNotThrow(() -> useCase.execute(input)); + + verify(memberGateway, times(1)).findById(eq(expectedMemberId)); + verify(memberGateway, times(1)).findById(any()); + + verify(memberGateway, times(0)).update(any()); + + verify(memberGateway, times(1)).create(argThat(member -> { + assertEquals(expectedMemberId, member.getId()); + assertEquals(expectedUsername, member.getUsername()); + assertEquals(expectedNickname, member.getNickname()); + assertEquals(expectedQuota, member.getQuota()); + assertEquals(expectedCreatedAt, member.getCreatedAt()); + assertEquals(expectedUpdatedAt, member.getUpdatedAt()); + assertEquals(expectedSynchronizedVersion, member.getSynchronizedVersion()); + return true; + })); + verify(memberGateway, times(1)).create(any()); + } + +} From 15379ace3e340d87ea37fe999edf37bc4d7a0423 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 13:49:59 -0300 Subject: [PATCH 31/89] refactor: update ListRequestQuotaUseCase and related classes to use ListRequestQuotaOutput --- .../quota/request/list/DefaultListRequestQuotaUseCase.java | 4 ++-- ...uestQuotaListOutput.java => ListRequestQuotaOutput.java} | 6 +++--- .../member/quota/request/list/ListRequestQuotaUseCase.java | 2 +- .../infrastructure/member/presenter/MemberPresenter.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename application/src/main/java/com/callv2/drive/application/member/quota/request/list/{RequestQuotaListOutput.java => ListRequestQuotaOutput.java} (78%) diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/DefaultListRequestQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/DefaultListRequestQuotaUseCase.java index e58969a1..10fe5b5f 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/DefaultListRequestQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/DefaultListRequestQuotaUseCase.java @@ -15,10 +15,10 @@ public DefaultListRequestQuotaUseCase(final MemberGateway memberGateway) { } @Override - public Page execute(final SearchQuery searchQuery) { + public Page execute(final SearchQuery searchQuery) { return memberGateway .findAllQuotaRequests(searchQuery) - .map(RequestQuotaListOutput::from); + .map(ListRequestQuotaOutput::from); } } diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/RequestQuotaListOutput.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java similarity index 78% rename from application/src/main/java/com/callv2/drive/application/member/quota/request/list/RequestQuotaListOutput.java rename to application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java index ed551e1d..b971d61e 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/RequestQuotaListOutput.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java @@ -5,14 +5,14 @@ import com.callv2.drive.domain.member.QuotaRequestPreview; import com.callv2.drive.domain.member.QuotaUnit; -public record RequestQuotaListOutput( +public record ListRequestQuotaOutput( String memberId, long amount, QuotaUnit unit, Instant requestedAt) { - public static RequestQuotaListOutput from(final QuotaRequestPreview quotaRequestPreview) { - return new RequestQuotaListOutput( + public static ListRequestQuotaOutput from(final QuotaRequestPreview quotaRequestPreview) { + return new ListRequestQuotaOutput( quotaRequestPreview.memberId(), quotaRequestPreview.amount(), quotaRequestPreview.unit(), diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaUseCase.java index b945ad76..5eff4bb0 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaUseCase.java @@ -4,6 +4,6 @@ import com.callv2.drive.domain.pagination.Page; import com.callv2.drive.domain.pagination.SearchQuery; -public abstract class ListRequestQuotaUseCase extends UseCase> { +public abstract class ListRequestQuotaUseCase extends UseCase> { } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/presenter/MemberPresenter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/presenter/MemberPresenter.java index a57a7ce2..a90cf289 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/presenter/MemberPresenter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/presenter/MemberPresenter.java @@ -1,13 +1,13 @@ package com.callv2.drive.infrastructure.member.presenter; -import com.callv2.drive.application.member.quota.request.list.RequestQuotaListOutput; +import com.callv2.drive.application.member.quota.request.list.ListRequestQuotaOutput; import com.callv2.drive.application.member.quota.retrieve.get.GetQuotaOutput; import com.callv2.drive.infrastructure.member.model.MemberQuotaResponse; import com.callv2.drive.infrastructure.member.model.QuotaRequestListResponse; public interface MemberPresenter { - static QuotaRequestListResponse present(RequestQuotaListOutput output) { + static QuotaRequestListResponse present(ListRequestQuotaOutput output) { return new QuotaRequestListResponse( output.memberId(), output.amount(), From 7d28ecc36fedc5f251179270f41baacc8db6f1d5 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 18:53:39 -0300 Subject: [PATCH 32/89] feat: Introduce Validatable interface and refactor ValueObject to implement it refactor: Update exception hierarchy to use SilentDomainException and VerboseDomainException refactor: Replace Error class with ValidationError for improved validation handling fix: Update validation logic in various domain classes to utilize new ValidationError chore: Clean up unused imports and improve code readability across domain classes fix: Adjust ApiError to handle string messages instead of Error objects --- .../file/create/DefaultCreateFileUseCase.java | 7 +-- .../create/DefaultCreateFolderUseCase.java | 4 +- .../folder/move/DefaultMoveFolderUseCase.java | 5 +- .../get/DefaultGetFileContentUseCaseTest.java | 7 ++- .../create/DefaultCreateFileUseCaseTest.java | 18 ++++-- .../get/DefaultGetFileUseCaseTest.java | 4 +- .../get/DefaultGetFolderUseCaseTest.java | 9 ++- .../java/com/callv2/drive/domain/Entity.java | 6 +- .../com/callv2/drive/domain/Identifier.java | 6 +- .../com/callv2/drive/domain/Validatable.java | 10 ++++ .../com/callv2/drive/domain/ValueObject.java | 4 +- .../exception/AlreadyExistsException.java | 32 +++------- .../domain/exception/DomainException.java | 60 +++++++++++++++---- .../domain/exception/IdMismatchException.java | 18 ++++-- .../exception/InternalErrorException.java | 26 ++++++-- .../exception/NoStacktraceException.java | 13 ---- .../domain/exception/NotFoundException.java | 32 +++------- .../exception/QuotaExceededException.java | 17 ++++-- .../exception/SilentDomainException.java | 17 ++++++ .../SynchronizedVersionOutdatedException.java | 25 ++++---- .../domain/exception/ValidationException.java | 33 +++++++--- .../exception/VerboseDomainException.java | 13 ++++ .../com/callv2/drive/domain/file/Content.java | 10 ++-- .../callv2/drive/domain/file/FileName.java | 10 ++-- .../drive/domain/file/FileValidator.java | 8 +-- .../callv2/drive/domain/folder/Folder.java | 25 +++++--- .../drive/domain/folder/FolderName.java | 15 ++--- .../drive/domain/folder/FolderValidator.java | 6 +- .../callv2/drive/domain/member/Member.java | 13 ++-- .../drive/domain/member/MemberValidator.java | 14 ++--- .../callv2/drive/domain/member/Nickname.java | 6 +- .../com/callv2/drive/domain/member/Quota.java | 10 ++-- .../drive/domain/member/QuotaRequest.java | 6 +- .../callv2/drive/domain/member/Username.java | 8 +-- .../drive/domain/pagination/Filter.java | 10 ++-- .../callv2/drive/domain/pagination/Page.java | 4 +- .../callv2/drive/domain/validation/Error.java | 9 --- .../domain/validation/ValidationError.java | 19 ++++++ .../domain/validation/ValidationHandler.java | 8 +-- .../drive/domain/validation/Validator.java | 4 +- .../validation/handler/Notification.java | 36 +++++------ .../api/controller/ApiError.java | 5 +- 42 files changed, 350 insertions(+), 242 deletions(-) create mode 100644 domain/src/main/java/com/callv2/drive/domain/Validatable.java delete mode 100644 domain/src/main/java/com/callv2/drive/domain/exception/NoStacktraceException.java create mode 100644 domain/src/main/java/com/callv2/drive/domain/exception/SilentDomainException.java create mode 100644 domain/src/main/java/com/callv2/drive/domain/exception/VerboseDomainException.java delete mode 100644 domain/src/main/java/com/callv2/drive/domain/validation/Error.java create mode 100644 domain/src/main/java/com/callv2/drive/domain/validation/ValidationError.java diff --git a/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java index bfa504aa..7cc89042 100644 --- a/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java @@ -20,7 +20,7 @@ import com.callv2.drive.domain.member.MemberGateway; import com.callv2.drive.domain.member.MemberID; import com.callv2.drive.domain.storage.StorageService; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; public class DefaultCreateFileUseCase extends CreateFileUseCase { @@ -58,8 +58,7 @@ public CreateFileOutput execute(final CreateFileInput input) { .sum(); if (actualUsedQuota + input.size() > owner.getQuota().sizeInBytes()) - throw QuotaExceededException.with("Quota exceeded", - Error.with("You have exceeded your actual quota of " + owner.getQuota().sizeInBytes() + " bytes")); + throw QuotaExceededException.with(owner.getQuota()); final FolderID folderId = folderGateway .findById(FolderID.of(input.folderId())) @@ -76,7 +75,7 @@ public CreateFileOutput execute(final CreateFileInput input) { final List filesOnSameFolder = fileGateway.findByFolder(folderId); if (filesOnSameFolder.stream().map(File::getName).anyMatch(fileName::equals)) throw ValidationException.with("Could not create Aggregate File", - Error.with("File with same name already exists on this folder")); + ValidationError.with("File with same name already exists on this folder")); final String randomContentName = UUID.randomUUID().toString(); final String contentLocation = storeContentFile(randomContentName, input.content()); diff --git a/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java index 8d67ff3c..f1de2564 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java @@ -9,7 +9,7 @@ import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; import com.callv2.drive.domain.member.MemberID; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; public class DefaultCreateFolderUseCase extends CreateFolderUseCase { @@ -41,7 +41,7 @@ public CreateFolderOutput execute(final CreateFolderInput input) { private Folder createFolder(final MemberID ownerId, FolderName name, final Folder parentFolder) { final Notification notification = Notification.create(); if (parentFolder.getSubFolders().stream().anyMatch(subFolder -> subFolder.name().equals(name))) - notification.append(Error.with("Folder with the same name already exists")); + notification.append(ValidationError.with("Folder with the same name already exists")); final Folder folder = notification.valdiate(() -> Folder.create(ownerId, name, parentFolder)); diff --git a/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java index b8a17411..b0d2dde2 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java @@ -8,7 +8,7 @@ import com.callv2.drive.domain.folder.Folder; import com.callv2.drive.domain.folder.FolderGateway; import com.callv2.drive.domain.folder.FolderID; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; public class DefaultMoveFolderUseCase extends MoveFolderUseCase { @@ -27,8 +27,9 @@ public void execute(MoveFolderInput input) { final Folder folder = findFolder(folderId); final Folder newParentFolder = findFolder(newParentFolderId); + // TODO inform the cause if (!canMove(folder, newParentFolder)) - throw ValidationException.with("Invalid move operation", Error.with("Cannot move folder")); + throw ValidationException.with("Invalid move operation", ValidationError.with("Cannot move folder")); final Folder oldParentFolder = findFolder(folder.getParentFolder()); oldParentFolder.removeSubFolder(folder); diff --git a/application/src/test/java/com/callv2/drive/application/file/content/get/DefaultGetFileContentUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/content/get/DefaultGetFileContentUseCaseTest.java index ee9d9e82..fa228678 100644 --- a/application/src/test/java/com/callv2/drive/application/file/content/get/DefaultGetFileContentUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/content/get/DefaultGetFileContentUseCaseTest.java @@ -65,8 +65,9 @@ void givenAnInexistentId_whenCallsExecute_shouldThrowNotFoundException() { final var expectedFileId = FileID.unique(); - final var expectedExceptionMessage = "File with id '%s' not found" - .formatted(expectedFileId.getValue().toString()); + final var expectedExceptionMessage = "[File] not found."; + final var expectedErrorsCount = 1; + final var expectedErrorMessage = "[File] with id [%s] not found.".formatted(expectedFileId.getValue()); when(fileGateway.findById(any())) .thenReturn(Optional.empty()); @@ -76,6 +77,8 @@ void givenAnInexistentId_whenCallsExecute_shouldThrowNotFoundException() { final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorsCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(fileGateway, times(1)).findById(any()); verify(fileGateway, times(1)).findById(eq(expectedFileId)); diff --git a/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java index 9dbe7950..f93c2c82 100644 --- a/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java @@ -167,8 +167,9 @@ void givenAnInvalidFolderId_whenCallsExecute_thenShouldThrowNotFoundException() final var expectedContent = new ByteArrayInputStream(contentBytes); final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Folder with id '%s' not found" - .formatted(expectedFolderId.getValue()); + final var expectedExceptionMessage = "[Folder] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Folder] with id [%s] not found.".formatted(expectedFolderId.getValue()); when(memberGateway.findById(any())) .thenReturn(Optional.of(owner)); @@ -187,6 +188,8 @@ void givenAnInvalidFolderId_whenCallsExecute_thenShouldThrowNotFoundException() final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(folderGateway, times(1)).findById(any()); verify(folderGateway, times(1)).findById(eq(expectedFolderId)); @@ -211,8 +214,9 @@ void givenAnInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() final var expectedContent = new ByteArrayInputStream(contentBytes); final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Member with id '%s' not found" - .formatted(expectedOwnerId.getValue()); + final var expectedExceptionMessage = "[Member] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Member] with id [%s] not found.".formatted(expectedOwnerId.getValue()); when(memberGateway.findById(any())) .thenReturn(Optional.empty()); @@ -228,6 +232,8 @@ void givenAnInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); @@ -623,9 +629,9 @@ void givenAValidParams_whenCallsExecuteAndMemberQuotaIsExceeded_thenShouldThrows final var expectedContent = new ByteArrayInputStream(contentBytes); final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Quota exceeded"; + final var expectedExceptionMessage = "Quota exceeded."; final var expectedErrorCount = 1; - final var expectedErrorMessage = "You have exceeded your actual quota of 1 bytes"; + final var expectedErrorMessage = "You have exceeded your current quota of 1 BYTE"; when(memberGateway.findById(ownerId)) .thenReturn(Optional.of(owner)); diff --git a/application/src/test/java/com/callv2/drive/application/file/retrieve/get/DefaultGetFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/retrieve/get/DefaultGetFileUseCaseTest.java index 6f93cbd9..c5f46bee 100644 --- a/application/src/test/java/com/callv2/drive/application/file/retrieve/get/DefaultGetFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/retrieve/get/DefaultGetFileUseCaseTest.java @@ -76,9 +76,9 @@ void givenAInvalidId_whenCallsExecute_thenShouldReturnNotFound() { final var expectedId = FileID.unique(); - final var expectedExceptionMessage = "File with id '%s' not found".formatted(expectedId.getValue().toString()); + final var expectedExceptionMessage = "[File] not found."; final var expectedErrorsCount = 1; - final var expectedErrorMessage = "File with id '%s' not found".formatted(expectedId.getValue().toString()); + final var expectedErrorMessage = "[File] with id [%s] not found.".formatted(expectedId.getValue().toString()); when(fileGateway.findById(any())) .thenReturn(Optional.empty()); diff --git a/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java index 68e524fc..963d79b5 100644 --- a/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java @@ -79,8 +79,9 @@ void givenNotExistentFolderId_whenCallsExecute_thenShouldThorwsNotFoundException final var expectedFolderId = FolderID.unique(); - final var excpectedExceptionMessage = "Folder with id '%s' not found" - .formatted(expectedFolderId.getValue().toString()); + final var expectedExceptionMessage = "[Folder] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Folder] with id [%s] not found.".formatted(expectedFolderId.getValue()); when(folderGateway.findById(expectedFolderId)) .thenReturn(Optional.empty()); @@ -89,7 +90,9 @@ void givenNotExistentFolderId_whenCallsExecute_thenShouldThorwsNotFoundException final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); - assertEquals(excpectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(folderGateway, times(1)).findById(any()); verify(folderGateway, times(1)).findById(eq(expectedFolderId)); diff --git a/domain/src/main/java/com/callv2/drive/domain/Entity.java b/domain/src/main/java/com/callv2/drive/domain/Entity.java index b201ac1a..67a38a38 100644 --- a/domain/src/main/java/com/callv2/drive/domain/Entity.java +++ b/domain/src/main/java/com/callv2/drive/domain/Entity.java @@ -2,9 +2,7 @@ import java.util.Objects; -import com.callv2.drive.domain.validation.ValidationHandler; - -public abstract class Entity> { +public abstract class Entity> implements Validatable { protected final ID id; @@ -12,8 +10,6 @@ protected Entity(final ID id) { this.id = Objects.requireNonNull(id, "'id' should not be null"); } - public abstract void validate(ValidationHandler handler); - public ID getId() { return id; } diff --git a/domain/src/main/java/com/callv2/drive/domain/Identifier.java b/domain/src/main/java/com/callv2/drive/domain/Identifier.java index b53c9437..4dc7a04f 100644 --- a/domain/src/main/java/com/callv2/drive/domain/Identifier.java +++ b/domain/src/main/java/com/callv2/drive/domain/Identifier.java @@ -2,7 +2,7 @@ import java.util.Objects; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public abstract class Identifier implements ValueObject { @@ -18,9 +18,9 @@ public T getValue() { } @Override - public void validate(ValidationHandler aHandler) { + public void validate(ValidationHandler handler) { if (Objects.isNull(this.id)) - aHandler.append(new Error("'id' should not be null")); + handler.append(new ValidationError("'id' should not be null")); } @Override diff --git a/domain/src/main/java/com/callv2/drive/domain/Validatable.java b/domain/src/main/java/com/callv2/drive/domain/Validatable.java new file mode 100644 index 00000000..1e62a60b --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/Validatable.java @@ -0,0 +1,10 @@ +package com.callv2.drive.domain; + +import com.callv2.drive.domain.validation.ValidationHandler; + +@FunctionalInterface +public interface Validatable { + + void validate(ValidationHandler handler); + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/ValueObject.java b/domain/src/main/java/com/callv2/drive/domain/ValueObject.java index f95bd954..374aa8d5 100644 --- a/domain/src/main/java/com/callv2/drive/domain/ValueObject.java +++ b/domain/src/main/java/com/callv2/drive/domain/ValueObject.java @@ -2,9 +2,9 @@ import com.callv2.drive.domain.validation.ValidationHandler; -public interface ValueObject { +public interface ValueObject extends Validatable { - default void validate(ValidationHandler aHandler) { + default void validate(ValidationHandler handler) { }; } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java b/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java index 73dfecd1..3efe5bf1 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java @@ -3,35 +3,21 @@ import java.util.List; import com.callv2.drive.domain.Entity; -import com.callv2.drive.domain.Identifier; -import com.callv2.drive.domain.validation.Error; -public class AlreadyExistsException extends DomainException { +public class AlreadyExistsException extends SilentDomainException { - private AlreadyExistsException(final String message) { - super(message, List.of(Error.with(message))); - } - - public static AlreadyExistsException with( - final Class>> entityClass) { - - final String message = String.format( - "%s already exists", - entityClass.getSimpleName()); - - return new AlreadyExistsException(message); + private AlreadyExistsException( + final Class> entityClass, + final String id) { + super("[%s] already exists.".formatted(entityClass.getSimpleName()), + List.of(DomainException.Error + .with("[%s] with id [%s] already exists.".formatted(entityClass.getSimpleName())))); } public static AlreadyExistsException with( - final Class>> entityClass, + final Class> entityClass, final String id) { - - final String message = String.format( - "%s with id '%s' already exists", - entityClass.getSimpleName(), - id); - - return new AlreadyExistsException(message); + return new AlreadyExistsException(entityClass, id); } } \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/DomainException.java b/domain/src/main/java/com/callv2/drive/domain/exception/DomainException.java index 77b742b5..c52631e4 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/DomainException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/DomainException.java @@ -1,28 +1,62 @@ package com.callv2.drive.domain.exception; +import java.util.ArrayList; import java.util.List; -import com.callv2.drive.domain.validation.Error; +public abstract class DomainException extends RuntimeException { -public class DomainException extends NoStacktraceException { + private final List errors; - protected final List errors; + protected DomainException( + final String message, + final List errors, + final Throwable cause, + final boolean verbose) { + super(message, cause, enableSuppression(verbose), writableStackTrace(verbose)); + this.errors = addCauseToErrors(errors, cause) == null ? List.of() : new ArrayList<>(errors); + } + + public List getErrors() { + return List.copyOf(errors); + } - protected DomainException(final String aMessage, final List anErrors) { - super(aMessage); - this.errors = anErrors; + public String errorsToString() { + return "[" + errors.stream() + .map(DomainException.Error::message) + .reduce((a, b) -> a + ", " + b) + .orElse("No errors") + "]"; } - public static DomainException with(final Error anError) { - return new DomainException(anError.message(), List.of(anError)); + private static boolean enableSuppression(final boolean verbose) { + return verbose ? false : true; } - public static DomainException with(final List anErrors) { - return new DomainException("", anErrors); + private static boolean writableStackTrace(final boolean verbose) { + return verbose ? true : false; } - public List getErrors() { - return errors; + private static List addCauseToErrors( + final List errors, + final Throwable cause) { + if (cause == null) + return errors; + + final List result = new ArrayList<>(errors != null ? errors : List.of()); + result.add(DomainException.Error.with(cause)); + return result; + } + + public record Error(String message) { + + public static DomainException.Error with(final String message) { + return new DomainException.Error(message); + } + + public static DomainException.Error with(final Throwable cause) { + return new DomainException.Error( + "Exception:[" + cause.getClass().getName() + "] Message:[" + cause.getMessage() + "]"); + } + } -} +} \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java b/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java index dd7ff28e..f43270e0 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java @@ -2,16 +2,22 @@ import java.util.List; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.Entity; -public class IdMismatchException extends DomainException { +public class IdMismatchException extends SilentDomainException { - private IdMismatchException(final String aMessage, final List errors) { - super(aMessage, errors); + private IdMismatchException( + final Class> entityClass, + final String id) { + super("[%s]'s id doesn't match.".formatted(entityClass.getSimpleName()), + List.of(DomainException.Error + .with("[%s] with id [%s] doesn't match.".formatted(entityClass.getSimpleName(), id)))); } - public static IdMismatchException with(final String aMessage, final List errors) { - return new IdMismatchException(aMessage, errors); + public static IdMismatchException with( + final Class> entityClass, + final String id) { + return new IdMismatchException(entityClass, id); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/InternalErrorException.java b/domain/src/main/java/com/callv2/drive/domain/exception/InternalErrorException.java index c28ebfdd..686111bf 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/InternalErrorException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/InternalErrorException.java @@ -1,13 +1,29 @@ package com.callv2.drive.domain.exception; -public class InternalErrorException extends NoStacktraceException { +import java.util.List; - protected InternalErrorException(final String aMessage, final Throwable cause) { - super(aMessage, cause); +public class InternalErrorException extends VerboseDomainException { + + protected InternalErrorException( + final String message, + final List errors, + final Throwable cause) { + super(message, errors, cause); + } + + public static InternalErrorException with(final Throwable cause) { + return new InternalErrorException(cause.getMessage(), List.of(), cause); + } + + public static InternalErrorException with(final String message, final Throwable cause) { + return new InternalErrorException(message, List.of(), cause); } - public static InternalErrorException with(final String aMessage, final Throwable cause) { - return new InternalErrorException(aMessage, cause); + public static InternalErrorException with( + final String message, + final List errors, + final Throwable cause) { + return new InternalErrorException(message, errors, cause); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/NoStacktraceException.java b/domain/src/main/java/com/callv2/drive/domain/exception/NoStacktraceException.java deleted file mode 100644 index 57aa8e1d..00000000 --- a/domain/src/main/java/com/callv2/drive/domain/exception/NoStacktraceException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.callv2.drive.domain.exception; - -public abstract class NoStacktraceException extends RuntimeException { - - protected NoStacktraceException(final String message) { - this(message, null); - } - - protected NoStacktraceException(final String message, final Throwable cause) { - super(message, cause, true, false); - } - -} diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/NotFoundException.java b/domain/src/main/java/com/callv2/drive/domain/exception/NotFoundException.java index 99f6f90a..aced298a 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/NotFoundException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/NotFoundException.java @@ -3,35 +3,21 @@ import java.util.List; import com.callv2.drive.domain.Entity; -import com.callv2.drive.domain.Identifier; -import com.callv2.drive.domain.validation.Error; -public class NotFoundException extends DomainException { +public class NotFoundException extends SilentDomainException { - private NotFoundException(final String message) { - super(message, List.of(Error.with(message))); - } - - public static NotFoundException with( - final Class>> entityClass) { - - final String message = String.format( - "%s not found", - entityClass.getSimpleName()); - - return new NotFoundException(message); + private NotFoundException( + final Class> entityClass, + final String id) { + super("[%s] not found.".formatted(entityClass.getSimpleName()), + List.of(DomainException.Error + .with("[%s] with id [%s] not found.".formatted(entityClass.getSimpleName(), id)))); } public static NotFoundException with( - final Class>> entityClass, + final Class> entityClass, final String id) { - - final String message = String.format( - "%s with id '%s' not found", - entityClass.getSimpleName(), - id); - - return new NotFoundException(message); + return new NotFoundException(entityClass, id); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/QuotaExceededException.java b/domain/src/main/java/com/callv2/drive/domain/exception/QuotaExceededException.java index 97c23948..e462be34 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/QuotaExceededException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/QuotaExceededException.java @@ -2,15 +2,20 @@ import java.util.List; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.member.Quota; -public class QuotaExceededException extends DomainException { +public class QuotaExceededException extends SilentDomainException { - private QuotaExceededException(final String aMessage, final List anErrors) { - super(aMessage, List.copyOf(anErrors)); + private QuotaExceededException(final Quota actualQuota) { + super( + "Quota exceeded.", + List.of(DomainException.Error.with("You have exceeded your current quota of " + + actualQuota.amount() + + " " + actualQuota.unit().name()))); } - public static QuotaExceededException with(final String message, final Error error) { - return new QuotaExceededException(message, List.of(error)); + public static QuotaExceededException with(final Quota actualQuota) { + return new QuotaExceededException(actualQuota); } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/SilentDomainException.java b/domain/src/main/java/com/callv2/drive/domain/exception/SilentDomainException.java new file mode 100644 index 00000000..6b14ff26 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/SilentDomainException.java @@ -0,0 +1,17 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +public abstract class SilentDomainException extends DomainException { + + private static final boolean VERBOSE = false; + + protected SilentDomainException(String message, List errors) { + super(message, errors, null, VERBOSE); + } + + protected SilentDomainException(String message, List errors, Throwable cause) { + super(message, errors, cause, VERBOSE); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java b/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java index 1af1a077..a958291c 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java @@ -2,20 +2,25 @@ import java.util.List; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.Entity; -public class SynchronizedVersionOutdatedException extends DomainException { +public class SynchronizedVersionOutdatedException extends SilentDomainException { - private SynchronizedVersionOutdatedException(final String aMessage, final List anErrors) { - super(aMessage, anErrors); + private SynchronizedVersionOutdatedException( + final Class> entityClass, + final Long actualEntityVersion, + final Long outdatedVersionProvided) { + super( + "Outdated version provided", + List.of(DomainException.Error.with("[%s] with actual version [%d] is outdated, but [%d] was provided." + .formatted(entityClass.getSimpleName(), actualEntityVersion, outdatedVersionProvided)))); } - public static SynchronizedVersionOutdatedException with(final String aMessage) { - return new SynchronizedVersionOutdatedException(aMessage, List.of()); - } - - public static SynchronizedVersionOutdatedException with(final String aMessage, final List anErrors) { - return new SynchronizedVersionOutdatedException(aMessage, anErrors); + public static SynchronizedVersionOutdatedException with( + final Class> entityClass, + final Long actualEntityVersion, + final Long outdatedVersionProvided) { + return new SynchronizedVersionOutdatedException(entityClass, actualEntityVersion, outdatedVersionProvided); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/ValidationException.java b/domain/src/main/java/com/callv2/drive/domain/exception/ValidationException.java index 2cee5da9..04a83af1 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/ValidationException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/ValidationException.java @@ -2,21 +2,40 @@ import java.util.List; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; -public class ValidationException extends DomainException { +public class ValidationException extends SilentDomainException { - private ValidationException(final String aMessage, final List anErrors) { - super(aMessage, List.copyOf(anErrors)); + private ValidationException(final String message, final List errors) { + super(message, List.copyOf(errors)); } - public static ValidationException with(final String aMessage, final Notification aNotification) { - return new ValidationException(aMessage, List.copyOf(aNotification.getErrors())); + public static ValidationException with(final String message, final Notification aNotification) { + return new ValidationException( + message, + aNotification + .getErrors() + .stream() + .map(ValidationError::toDomainError) + .toList()); } - public static ValidationException with(final String message, final Error error) { + public static ValidationException with(final String message, final DomainException.Error error) { return new ValidationException(message, List.of(error)); } + public static ValidationException with(final String message, final ValidationError error) { + return new ValidationException(message, List.of(error.toDomainError())); + } + + public static ValidationException with(final String message, final List errors) { + return new ValidationException( + message, + errors + .stream() + .map(ValidationError::toDomainError) + .toList()); + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/VerboseDomainException.java b/domain/src/main/java/com/callv2/drive/domain/exception/VerboseDomainException.java new file mode 100644 index 00000000..658b2720 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/VerboseDomainException.java @@ -0,0 +1,13 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +public class VerboseDomainException extends DomainException { + + private static final boolean VERBOSE = true; + + protected VerboseDomainException(String message, List errors, Throwable cause) { + super(message, errors, cause, VERBOSE); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/file/Content.java b/domain/src/main/java/com/callv2/drive/domain/file/Content.java index 692df344..b63b1b6e 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/Content.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/Content.java @@ -2,7 +2,7 @@ import com.callv2.drive.domain.ValueObject; import com.callv2.drive.domain.validation.ValidationHandler; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; public record Content(String location, String type, long size) implements ValueObject { @@ -18,24 +18,24 @@ public void validate(final ValidationHandler aHandler) { private void validateLocation(final ValidationHandler aHandler) { if (location == null) { - aHandler.append(new Error("'location' cannot be null.")); + aHandler.append(new ValidationError("'location' cannot be null.")); return; } if (location.trim().isEmpty()) { - aHandler.append(new Error("'location' cannot be empty.")); + aHandler.append(new ValidationError("'location' cannot be empty.")); return; } } private void validateType(final ValidationHandler aHandler) { if (type == null) { - aHandler.append(new Error("'type' cannot be null.")); + aHandler.append(new ValidationError("'type' cannot be null.")); return; } if (type.trim().isEmpty()) { - aHandler.append(new Error("'type' cannot be empty.")); + aHandler.append(new ValidationError("'type' cannot be empty.")); return; } } diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileName.java b/domain/src/main/java/com/callv2/drive/domain/file/FileName.java index 9f8acb30..9b2ab8bf 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileName.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileName.java @@ -1,7 +1,7 @@ package com.callv2.drive.domain.file; import com.callv2.drive.domain.ValueObject; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public record FileName(String value) implements ValueObject { @@ -30,22 +30,22 @@ public static FileName of(final String value) { public void validate(final ValidationHandler aHandler) { if (value == null) { - aHandler.append(new Error("'name' cannot be null.")); + aHandler.append(new ValidationError("'name' cannot be null.")); return; } if (value.trim().isEmpty()) { - aHandler.append(new Error("'name' cannot be empty.")); + aHandler.append(new ValidationError("'name' cannot be empty.")); return; } String trimmedName = value.trim(); if (trimmedName.length() < MIN_LENGTH || trimmedName.length() > MAX_LENGTH) aHandler.append( - new Error("'name' must be between " + MIN_LENGTH + " and " + MAX_LENGTH + " characters.")); + new ValidationError("'name' must be between " + MIN_LENGTH + " and " + MAX_LENGTH + " characters.")); if (isReservedName(trimmedName)) - aHandler.append(new Error("'name' cannot be a reserved name: " + trimmedName)); + aHandler.append(new ValidationError("'name' cannot be a reserved name: " + trimmedName)); } private boolean isReservedName(final String name) { diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileValidator.java b/domain/src/main/java/com/callv2/drive/domain/file/FileValidator.java index be3de072..7c4ed141 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileValidator.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileValidator.java @@ -1,6 +1,6 @@ package com.callv2.drive.domain.file; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.Validator; @@ -23,7 +23,7 @@ public void validate() { private void validateId() { if (this.file.getId() == null) { - this.validationHandler().append(new Error("'id' should not be null")); + this.validationHandler().append(new ValidationError("'id' should not be null")); return; } @@ -33,7 +33,7 @@ private void validateId() { private void validateName() { if (this.file.getName() == null) { - this.validationHandler().append(new Error("'name' should not be null")); + this.validationHandler().append(new ValidationError("'name' should not be null")); return; } @@ -43,7 +43,7 @@ private void validateName() { private void validateContent() { if (this.file.getContent() == null) { - this.validationHandler().append(new Error("'content' should not be null")); + this.validationHandler().append(new ValidationError("'content' should not be null")); return; } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java b/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java index 39a42fda..5dfa0ed5 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java @@ -7,7 +7,7 @@ import com.callv2.drive.domain.AggregateRoot; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.member.MemberID; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.handler.Notification; @@ -119,13 +119,16 @@ public Folder changeParentFolder(final Folder parentFolder) { if (this.parentFolder.equals(parentFolder.getId())) return this; + final Notification notification = Notification.create(); + if (this.subFolders.stream().anyMatch(it -> it.id().equals(parentFolder.getId()))) - throw ValidationException.with("Error on change parent folder", - Error.with("Parent folder cannot be a subfolder")); + notification.append(ValidationError.with("Parent folder cannot be a subfolder")); if (this.getId().equals(parentFolder.getId())) - throw ValidationException.with("Error on change parent folder", - Error.with("Parent folder cannot be the same folder")); + notification.append(ValidationError.with("Parent folder cannot be the same folder")); + + if (notification.hasError()) + throw ValidationException.with("Error on change parent folder", notification); this.parentFolder = parentFolder.getId(); this.updatedAt = Instant.now(); @@ -139,12 +142,18 @@ public Folder addSubFolder(final Folder folder) { final SubFolder subFolder = SubFolder.from(folder); + final Notification notification = Notification.create(); + if (this.subFolders.contains(subFolder)) - throw ValidationException.with("Error on add subfolder", Error.with("SubFolder already exists")); + notification + .append(ValidationError.with("SubFolder already exists")); if (this.subFolders.stream().anyMatch(it -> it.name().equals(folder.getName()))) - throw ValidationException.with("Error on add subfolder", - Error.with("SubFolder %s already exists".formatted(folder.getName().value()))); + notification + .append(ValidationError.with("SubFolder %s already exists".formatted(folder.getName().value()))); + + if (notification.hasError()) + throw ValidationException.with("Error on add subfolder", notification); this.subFolders.add(subFolder); this.updatedAt = Instant.now(); diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/FolderName.java b/domain/src/main/java/com/callv2/drive/domain/folder/FolderName.java index 590acf6e..d0efec0d 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/FolderName.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/FolderName.java @@ -4,7 +4,7 @@ import java.util.regex.Pattern; import com.callv2.drive.domain.ValueObject; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public record FolderName(String value) implements ValueObject { @@ -20,25 +20,26 @@ public static FolderName of(final String value) { } @Override - public void validate(ValidationHandler aHandler) { + public void validate(ValidationHandler handler) { if (value == null) { - aHandler.append(new Error("'name' cannot be null.")); + handler.append(new ValidationError("'name' cannot be null.")); return; } if (value.trim().isEmpty()) { - aHandler.append(new Error("'name' cannot be empty.")); + handler.append(new ValidationError("'name' cannot be empty.")); return; } String trimmedName = value.trim(); if (trimmedName.length() < MIN_LENGTH || trimmedName.length() > MAX_LENGTH) - aHandler.append( - new Error("'name' must be between " + MIN_LENGTH + " and " + MAX_LENGTH + " characters.")); + handler.append( + new ValidationError( + "'name' must be between " + MIN_LENGTH + " and " + MAX_LENGTH + " characters.")); if (containsInvalidCharacters(value)) - aHandler.append(new Error("'name' contains invalid characters.")); + handler.append(new ValidationError("'name' contains invalid characters.")); } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/FolderValidator.java b/domain/src/main/java/com/callv2/drive/domain/folder/FolderValidator.java index ee9b570b..c7b4f0c7 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/FolderValidator.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/FolderValidator.java @@ -7,9 +7,9 @@ public class FolderValidator extends Validator { private final Folder folder; - protected FolderValidator(final Folder aFolder, final ValidationHandler aHandler) { - super(aHandler); - this.folder = aFolder; + protected FolderValidator(final Folder folder, final ValidationHandler handler) { + super(handler); + this.folder = folder; } @Override diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index 924d1da1..cf4cd3e3 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -1,14 +1,12 @@ package com.callv2.drive.domain.member; import java.time.Instant; -import java.util.List; import java.util.Optional; import com.callv2.drive.domain.AggregateRoot; import com.callv2.drive.domain.exception.IdMismatchException; import com.callv2.drive.domain.exception.SynchronizedVersionOutdatedException; import com.callv2.drive.domain.exception.ValidationException; -import com.callv2.drive.domain.validation.Error; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.handler.Notification; @@ -65,13 +63,16 @@ public void validate(final ValidationHandler handler) { public Member synchronize(final Member member) { if (!this.id.equals(member.id)) - throw IdMismatchException - .with("Member ID mismatch", List.of(Error.with("Member ID does not match"))); + throw IdMismatchException.with( + Member.class, + member.id.getValue()); if (this.synchronizedVersion > member.synchronizedVersion) throw SynchronizedVersionOutdatedException - .with("Member synchronizedVersion mismatch", - List.of(Error.with("Member synchronizedVersion is outdated"))); + .with( + Member.class, + this.synchronizedVersion, + member.synchronizedVersion); this.nickname = member.nickname; this.username = member.username; diff --git a/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java b/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java index 1d1805d0..80ef73b1 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java @@ -1,6 +1,6 @@ package com.callv2.drive.domain.member; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.Validator; @@ -8,8 +8,8 @@ public class MemberValidator extends Validator { private final Member member; - protected MemberValidator(final Member aMember, final ValidationHandler aHandler) { - super(aHandler); + protected MemberValidator(final Member aMember, final ValidationHandler handler) { + super(handler); this.member = aMember; } @@ -24,7 +24,7 @@ public void validate() { private void validateId() { if (this.member.getId() == null) { - this.validationHandler().append(Error.with("'id' is required")); + this.validationHandler().append(ValidationError.with("'id' is required")); return; } @@ -33,7 +33,7 @@ private void validateId() { private void validateUsername() { if (this.member.getUsername() == null) { - this.validationHandler().append(Error.with("'username' is required")); + this.validationHandler().append(ValidationError.with("'username' is required")); return; } @@ -42,7 +42,7 @@ private void validateUsername() { private void validateNickname() { if (this.member.getNickname() == null) { - this.validationHandler().append(Error.with("'nickname' is required")); + this.validationHandler().append(ValidationError.with("'nickname' is required")); return; } @@ -51,7 +51,7 @@ private void validateNickname() { private void validateQuota() { if (this.member.getQuota() == null) { - this.validationHandler().append(Error.with("'quota' is required")); + this.validationHandler().append(ValidationError.with("'quota' is required")); return; } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java b/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java index aee84fc2..4a31f9ce 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java @@ -1,7 +1,7 @@ package com.callv2.drive.domain.member; import com.callv2.drive.domain.ValueObject; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public record Nickname(String value) implements ValueObject { @@ -11,9 +11,9 @@ public static Nickname of(final String nickname) { } @Override - public void validate(final ValidationHandler aHandler) { + public void validate(final ValidationHandler handler) { if (value == null || value.isBlank()) - aHandler.append(Error.with("'nickname' is required")); + handler.append(ValidationError.with("'nickname' is required")); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Quota.java b/domain/src/main/java/com/callv2/drive/domain/member/Quota.java index f2334f4f..0a5ed1de 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Quota.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Quota.java @@ -2,7 +2,7 @@ import java.util.Objects; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.ValueObject; import com.callv2.drive.domain.validation.ValidationHandler; @@ -17,15 +17,15 @@ public long sizeInBytes() { } @Override - public void validate(ValidationHandler aHandler) { + public void validate(ValidationHandler handler) { if (Objects.isNull(this.amount)) - aHandler.append(new Error("'amount' should not be null")); + handler.append(new ValidationError("'amount' should not be null")); if (!Objects.isNull(this.amount) && this.amount < 0) - aHandler.append(new Error("'amount' should be greater than 0")); + handler.append(new ValidationError("'amount' should be greater than 0")); if (Objects.isNull(this.unit)) - aHandler.append(new Error("'unit' should not be null")); + handler.append(new ValidationError("'unit' should not be null")); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequest.java b/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequest.java index 9adec269..74070279 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequest.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequest.java @@ -3,7 +3,7 @@ import java.time.Instant; import com.callv2.drive.domain.ValueObject; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public record QuotaRequest(Quota quota, Instant requesteddAt) implements ValueObject { @@ -15,10 +15,10 @@ public static QuotaRequest of(Quota quota, Instant requestedAt) { @Override public void validate(ValidationHandler handler) { if (quota == null) - handler.append(new Error("'quota' should not be null")); + handler.append(new ValidationError("'quota' should not be null")); if (requesteddAt == null) - handler.append(new Error("'requestedAt' should not be null")); + handler.append(new ValidationError("'requestedAt' should not be null")); if (quota != null) quota.validate(handler); diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Username.java b/domain/src/main/java/com/callv2/drive/domain/member/Username.java index 9f01578b..913b6d5f 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Username.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Username.java @@ -1,7 +1,7 @@ package com.callv2.drive.domain.member; import com.callv2.drive.domain.ValueObject; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public record Username(String value) implements ValueObject { @@ -11,12 +11,12 @@ public static Username of(final String username) { } @Override - public void validate(final ValidationHandler aHandler) { + public void validate(final ValidationHandler handler) { if (value == null || value.isBlank()) - aHandler.append(Error.with("'username' is required")); + handler.append(ValidationError.with("'username' is required")); if (value.contains(" ")) - aHandler.append(Error.with("'username' cannot contain spaces")); + handler.append(ValidationError.with("'username' cannot contain spaces")); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/pagination/Filter.java b/domain/src/main/java/com/callv2/drive/domain/pagination/Filter.java index eff893b5..71314b0e 100644 --- a/domain/src/main/java/com/callv2/drive/domain/pagination/Filter.java +++ b/domain/src/main/java/com/callv2/drive/domain/pagination/Filter.java @@ -4,7 +4,7 @@ import java.util.Optional; import com.callv2.drive.domain.exception.ValidationException; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; public record Filter(String field, String value, String valueToCompare, Type type) { @@ -13,17 +13,17 @@ public record Filter(String field, String value, String valueToCompare, Type typ final Notification notification = Notification.create(); if (field == null) - notification.append(Error.with("Filter.field cannot be null")); + notification.append(ValidationError.with("Filter.field cannot be null")); if (field != null && field.isBlank()) - notification.append(Error.with("Filter.field cannot be blank")); + notification.append(ValidationError.with("Filter.field cannot be blank")); if (value == null) - notification.append(Error.with("Filter.value cannot be null")); + notification.append(ValidationError.with("Filter.value cannot be null")); if (type == null) notification.append( - Error.with( + ValidationError.with( "Filter.type cannot be null, Valid values are: " + Arrays.toString(Type.values()))); if (notification.hasError()) diff --git a/domain/src/main/java/com/callv2/drive/domain/pagination/Page.java b/domain/src/main/java/com/callv2/drive/domain/pagination/Page.java index 0a26ea3b..55fb5841 100644 --- a/domain/src/main/java/com/callv2/drive/domain/pagination/Page.java +++ b/domain/src/main/java/com/callv2/drive/domain/pagination/Page.java @@ -11,10 +11,10 @@ public record Page( List items) { public Page map(final Function mapper) { - final List aNewList = this.items.stream() + final List newList = this.items.stream() .map(mapper) .toList(); - return new Page<>(currentPage(), perPage(), totalPages(), total(), aNewList); + return new Page<>(currentPage(), perPage(), totalPages(), total(), newList); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/Error.java b/domain/src/main/java/com/callv2/drive/domain/validation/Error.java deleted file mode 100644 index 7d649f01..00000000 --- a/domain/src/main/java/com/callv2/drive/domain/validation/Error.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.callv2.drive.domain.validation; - -public record Error(String message) { - - public static Error with(final String message) { - return new Error(message); - } - -} diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/ValidationError.java b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationError.java new file mode 100644 index 00000000..dcba3d38 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationError.java @@ -0,0 +1,19 @@ +package com.callv2.drive.domain.validation; + +import com.callv2.drive.domain.exception.DomainException; + +public record ValidationError(String message) { + + public static ValidationError with(final String message) { + return new ValidationError(message); + } + + public static ValidationError fromDomainError(final DomainException.Error domainError) { + return new ValidationError(domainError.message()); + } + + public DomainException.Error toDomainError() { + return DomainException.Error.with(message()); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java index 2c6f5afc..c4d1fc50 100644 --- a/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java +++ b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java @@ -4,13 +4,13 @@ public interface ValidationHandler { - ValidationHandler append(final Error anError); + ValidationHandler append(final ValidationError error); - ValidationHandler append(final ValidationHandler anHandler); + ValidationHandler append(final ValidationHandler handler); - T valdiate(final Validation aValidation); + T valdiate(final Validation validation); - List getErrors(); + List getErrors(); default boolean hasError() { return getErrors() != null && !getErrors().isEmpty(); diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/Validator.java b/domain/src/main/java/com/callv2/drive/domain/validation/Validator.java index 301722c9..4f891443 100644 --- a/domain/src/main/java/com/callv2/drive/domain/validation/Validator.java +++ b/domain/src/main/java/com/callv2/drive/domain/validation/Validator.java @@ -4,8 +4,8 @@ public abstract class Validator { private final ValidationHandler handler; - protected Validator(final ValidationHandler aHandler) { - this.handler = aHandler; + protected Validator(final ValidationHandler handler) { + this.handler = handler; } public abstract void validate(); diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java b/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java index 45abd598..4388d2a8 100644 --- a/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java +++ b/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java @@ -4,57 +4,53 @@ import java.util.List; import com.callv2.drive.domain.exception.DomainException; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public class Notification implements ValidationHandler { - private final List errors; + private final List errors; - private Notification(final List errors) { - this.errors = errors; + private Notification(final List errors) { + this.errors = errors == null ? new ArrayList<>() : errors; } public static Notification create() { return new Notification(new ArrayList<>()); } - public static Notification create(final Throwable t) { - return new Notification(new ArrayList<>()).append(new Error(t.getMessage())); - } - - public static Notification create(final Error anError) { - return new Notification(new ArrayList<>()).append(anError); + public static Notification create(final ValidationError error) { + return new Notification(new ArrayList<>()).append(error); } @Override - public Notification append(final Error anError) { - this.errors.add(anError); + public Notification append(final ValidationError error) { + this.errors.add(error); return this; } @Override - public Notification append(final ValidationHandler anHandler) { - this.errors.addAll(anHandler.getErrors()); + public Notification append(final ValidationHandler handler) { + this.errors.addAll(handler.getErrors()); return this; } @Override - public T valdiate(final Validation aValidation) { + public T valdiate(final Validation validation) { try { - return aValidation.validate(); + return validation.validate(); } catch (DomainException e) { - this.errors.addAll(e.getErrors()); + this.errors.addAll(e.getErrors().stream().map(ValidationError::fromDomainError).toList()); } catch (Throwable e) { - this.errors.add(new Error(e.getMessage())); + this.errors.add(new ValidationError(e.getMessage())); } return null; } @Override - public List getErrors() { - return this.errors; + public List getErrors() { + return this.errors == null ? List.of() : List.copyOf(this.errors); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/ApiError.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/ApiError.java index 06e7ffdc..4daf26da 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/ApiError.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/ApiError.java @@ -3,15 +3,14 @@ import java.util.List; import com.callv2.drive.domain.exception.DomainException; -import com.callv2.drive.domain.validation.Error; -public record ApiError(String message, List errors) { +public record ApiError(String message, List errors) { static ApiError with(final String message) { return new ApiError(message, List.of()); } static ApiError from(final DomainException ex) { - return new ApiError(ex.getMessage(), ex.getErrors()); + return new ApiError(ex.getMessage(), ex.getErrors().stream().map(DomainException.Error::message).toList()); } } \ No newline at end of file From 51193580efb4bdc9dfa2e7dff6747685179e1eaf Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 21:24:34 -0300 Subject: [PATCH 33/89] refactor: remove unnecessary null suppression annotation from ThrowableLogExecutor --- .../aop/aspects/executor/ThrowableLogExecutor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java index 6bef77d9..72cebda8 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java @@ -17,7 +17,6 @@ public static ThrowableLogExecutor create(final Level level, final Class claz return new ThrowableLogExecutor(level, clazz); } - @SuppressWarnings("null") @Override public void execute(final PostInvocationContext context) { if (context.getThrowable() != null) From 0811e4a2b0e41c0e13642dd0e55257de0cf631a7 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 21:47:25 -0300 Subject: [PATCH 34/89] refactor: rename parameter 'anIn' to 'input' in execute method for consistency --- .../src/main/java/com/callv2/drive/application/UnitUseCase.java | 2 +- .../src/main/java/com/callv2/drive/application/UseCase.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/UnitUseCase.java b/application/src/main/java/com/callv2/drive/application/UnitUseCase.java index 5d91159a..023e00c1 100644 --- a/application/src/main/java/com/callv2/drive/application/UnitUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/UnitUseCase.java @@ -2,6 +2,6 @@ public abstract class UnitUseCase { - public abstract void execute(IN anIn); + public abstract void execute(IN input); } diff --git a/application/src/main/java/com/callv2/drive/application/UseCase.java b/application/src/main/java/com/callv2/drive/application/UseCase.java index cb1cbd2b..13f5a9c3 100644 --- a/application/src/main/java/com/callv2/drive/application/UseCase.java +++ b/application/src/main/java/com/callv2/drive/application/UseCase.java @@ -2,6 +2,6 @@ public abstract class UseCase { - public abstract OUT execute(IN anIn); + public abstract OUT execute(IN input); } From c65b0b5ba9f3f9dc9a0d09ab1a6874dac2e9bab0 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 21:48:15 -0300 Subject: [PATCH 35/89] refactor: rename 'id' to 'memberId' in GetQuotaInput for clarity and consistency --- .../member/quota/retrieve/get/DefaultGetQuotaUseCase.java | 4 ++-- .../member/quota/retrieve/get/GetQuotaInput.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java index 03a58303..2cc9ac19 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java @@ -21,11 +21,11 @@ public DefaultGetQuotaUseCase(final MemberGateway memberGateway, final FileGatew @Override public GetQuotaOutput execute(GetQuotaInput input) { - final MemberID ownerId = MemberID.of(input.id()); + final MemberID ownerId = MemberID.of(input.memberId()); final Member owner = memberGateway .findById(ownerId) - .orElseThrow(() -> NotFoundException.with(Member.class, input.id().toString())); + .orElseThrow(() -> NotFoundException.with(Member.class, input.memberId().toString())); final Long actualUsedQuota = fileGateway .findByOwner(ownerId) diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaInput.java b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaInput.java index d20e9b0f..b8d6adad 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaInput.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaInput.java @@ -1,9 +1,9 @@ package com.callv2.drive.application.member.quota.retrieve.get; -public record GetQuotaInput(String id) { +public record GetQuotaInput(String memberId) { - public static GetQuotaInput of(String id) { - return new GetQuotaInput(id); + public static GetQuotaInput of(final String memberId) { + return new GetQuotaInput(memberId); } } From 017a6f9eb9603cc323a92e9c11d8d18741d4dba6 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 8 Jun 2025 22:20:34 -0300 Subject: [PATCH 36/89] refactor: update quota-related classes to improve member information handling and consistency --- .../request/list/ListRequestQuotaOutput.java | 16 ++++++++++------ .../retrieve/get/DefaultGetQuotaUseCase.java | 2 +- .../quota/retrieve/get/GetQuotaOutput.java | 6 +++--- .../drive/domain/member/QuotaRequestPreview.java | 8 +++++--- .../drive/infrastructure/api/MemberAdminAPI.java | 2 +- .../member/model/MemberQuotaResponse.java | 2 +- .../member/model/QuotaRequestListResponse.java | 5 ++++- .../member/persistence/MemberJpaRepository.java | 8 +++++--- .../member/presenter/MemberPresenter.java | 16 +++++++++------- 9 files changed, 39 insertions(+), 26 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java index b971d61e..304fd469 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java @@ -7,16 +7,20 @@ public record ListRequestQuotaOutput( String memberId, - long amount, - QuotaUnit unit, - Instant requestedAt) { + String memberUsername, + String memberNickname, + long quotaAmount, + QuotaUnit quotaUnit, + Instant quotaRequestedAt) { public static ListRequestQuotaOutput from(final QuotaRequestPreview quotaRequestPreview) { return new ListRequestQuotaOutput( quotaRequestPreview.memberId(), - quotaRequestPreview.amount(), - quotaRequestPreview.unit(), - quotaRequestPreview.requestedAt()); + quotaRequestPreview.memberUsername(), + quotaRequestPreview.memberNickname(), + quotaRequestPreview.quotaAmount(), + quotaRequestPreview.quotaUnit(), + quotaRequestPreview.quotaRequestedAt()); } } diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java index 2cc9ac19..5dacc890 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java @@ -36,7 +36,7 @@ public GetQuotaOutput execute(GetQuotaInput input) { final Long available = owner.getQuota().sizeInBytes() - actualUsedQuota; - return GetQuotaOutput.from(actualUsedQuota, owner.getQuota().sizeInBytes(), available); + return GetQuotaOutput.from(ownerId.getValue(), actualUsedQuota, owner.getQuota().sizeInBytes(), available); } } diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaOutput.java b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaOutput.java index cf401a69..bf305426 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaOutput.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaOutput.java @@ -1,9 +1,9 @@ package com.callv2.drive.application.member.quota.retrieve.get; -public record GetQuotaOutput(Long used, Long total, Long available) { +public record GetQuotaOutput(String memberId, Long used, Long total, Long available) { - public static GetQuotaOutput from(final Long used, final Long total, final Long available) { - return new GetQuotaOutput(used, total, available); + public static GetQuotaOutput from(final String memberId, final Long used, final Long total, final Long available) { + return new GetQuotaOutput(memberId, used, total, available); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequestPreview.java b/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequestPreview.java index 8cc8fc45..be178c5f 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequestPreview.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequestPreview.java @@ -4,8 +4,10 @@ public record QuotaRequestPreview( String memberId, - long amount, - QuotaUnit unit, - Instant requestedAt) { + String memberUsername, + String memberNickname, + long quotaAmount, + QuotaUnit quotaUnit, + Instant quotaRequestedAt) { } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java index f24c99a3..544dd3ec 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java @@ -54,7 +54,7 @@ ResponseEntity approveQuotaRequest( ResponseEntity> listQuotaRequests( @RequestParam(name = "page", required = false, defaultValue = "0") final int page, @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, - @RequestParam(name = "orderField", required = false, defaultValue = "requestedAt") String orderField, + @RequestParam(name = "orderField", required = false, defaultValue = "quotaRequestedAt") String orderField, @RequestParam(name = "orderDirection", required = false, defaultValue = "DESC") Pagination.Order.Direction orderDirection); } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/MemberQuotaResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/MemberQuotaResponse.java index 58db7ce3..c496b451 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/MemberQuotaResponse.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/MemberQuotaResponse.java @@ -1,5 +1,5 @@ package com.callv2.drive.infrastructure.member.model; -public record MemberQuotaResponse(Long used, Long total, Long available) { +public record MemberQuotaResponse(String memberId, Long used, Long total, Long available) { } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/QuotaRequestListResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/QuotaRequestListResponse.java index 815dc821..4ffbf416 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/QuotaRequestListResponse.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/QuotaRequestListResponse.java @@ -5,9 +5,12 @@ import com.callv2.drive.domain.member.QuotaUnit; public record QuotaRequestListResponse( - String memberId, + Member member, long amount, QuotaUnit unit, Instant requestedAt) { + public record Member(String id, String username) { + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java index 392098e6..efe13292 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java @@ -17,9 +17,11 @@ public interface MemberJpaRepository extends JpaRepository Date: Sun, 8 Jun 2025 22:43:15 -0300 Subject: [PATCH 37/89] feat: add AlreadyExistsException handling in member creation and synchronization --- .../drive/infrastructure/member/DefaultMemberGateway.java | 4 ++++ .../messaging/listener/member/MemberSyncListener.java | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java index 408bd28e..ff1a698c 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import com.callv2.drive.domain.exception.AlreadyExistsException; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; @@ -28,6 +29,9 @@ public DefaultMemberGateway(final MemberJpaRepository memberJpaRepository) { @Override public Member create(final Member member) { + if (this.memberJpaRepository.existsById(member.getId().getValue())) + throw AlreadyExistsException.with(Member.class, member.getId().getValue()); + return this.memberJpaRepository.save(MemberJpaEntity.fromDomain(member)).toDomain(); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java index 18b45dba..b48fcdcf 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java @@ -10,6 +10,7 @@ import com.callv2.drive.application.member.synchronize.SynchronizeMemberInput; import com.callv2.drive.application.member.synchronize.SynchronizeMemberUseCase; +import com.callv2.drive.domain.exception.AlreadyExistsException; import com.callv2.drive.domain.exception.IdMismatchException; import com.callv2.drive.domain.exception.SynchronizedVersionOutdatedException; import com.callv2.drive.infrastructure.messaging.listener.Listener; @@ -41,7 +42,10 @@ public void handle(final MemberSyncEvent data) { try { synchronizeMemberUseCase.execute(createMemberInput); - } catch (OptimisticLockingFailureException | SynchronizedVersionOutdatedException | IdMismatchException e) { + } catch (OptimisticLockingFailureException + | SynchronizedVersionOutdatedException + | IdMismatchException + | AlreadyExistsException e) { log.warn("Error synchronizing member: {}", e.getMessage(), e); } From 56f0cc4fb57214a4e0262c2519041e4b08180717 Mon Sep 17 00:00:00 2001 From: Artur Lampert Date: Mon, 9 Jun 2025 11:55:10 -0300 Subject: [PATCH 38/89] test: added second test --- .../delete/DefaultDeleteFileUseCaseTest.java | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java index 8de66908..a7888dac 100644 --- a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -1,5 +1,6 @@ package com.callv2.drive.application.file.delete; +import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.file.*; import com.callv2.drive.domain.folder.FolderID; import com.callv2.drive.domain.member.*; @@ -13,8 +14,9 @@ import java.time.Instant; import java.util.Optional; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class DefaultDeleteFileUseCaseTest { @@ -32,7 +34,7 @@ public class DefaultDeleteFileUseCaseTest { StorageService storageService; @Test - void givenAValidParam_whenExecute_thenShouldDeleteFile() { + void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { final Member owner = Member.create(MemberID.of("owner")) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) @@ -74,10 +76,49 @@ void givenAValidParam_whenExecute_thenShouldDeleteFile() { useCase.execute(input); - verify(fileGateway).deleteById(expectedFileId); - verify(storageService).delete(expectedContent.location()); + verify(memberGateway, times(1)).findById(any()); + verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(fileGateway, times(1)).findById(any()); + verify(fileGateway, times(1)).findById(eq(expectedFileId)); + verify(fileGateway, times(1)).deleteById(any()); + verify(fileGateway, times(1)).deleteById(eq(expectedFileId)); + verify(storageService, times(1)).delete(any()); + verify(storageService, times(1)).delete(eq(expectedContent.location())); + } + + @Test + void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { + + final MemberID expectedOwnerId = MemberID.of("owner"); + final FileID expectedFileId = FileID.unique(); + + final String expectedExceptionMessage = "Member with id '%s' not found" + .formatted(expectedOwnerId.getValue()); + + when(memberGateway.findById(expectedOwnerId)) + .thenReturn(Optional.empty()); + + final DeleteFileInput input = DeleteFileInput.of( + expectedOwnerId.getValue(), + expectedFileId.getValue() + ); + + final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + verify(memberGateway, times(1)).findById(any()); + verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(fileGateway, never()).findById(any()); + verify(fileGateway, never()).deleteById(any()); + verify(storageService, never()).delete(any()); } + @Test + void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException(){ + + } + + } From a25fa13e1285592e3151b8cc783c9c44d68b7ae1 Mon Sep 17 00:00:00 2001 From: Artur Lampert Date: Mon, 9 Jun 2025 15:34:04 -0300 Subject: [PATCH 39/89] feat: add system access handling to member and file deletion processes Co-authored-by: Jhonata Peres --- .../file/delete/DefaultDeleteFileUseCase.java | 4 +++- .../DefaultSynchronizeMemberUseCase.java | 1 + .../synchronize/SynchronizeMemberInput.java | 4 +++- .../delete/DefaultDeleteFileUseCaseTest.java | 12 +++++++++++- .../domain/exception/NotAllowedException.java | 17 +++++++++++++++++ .../com/callv2/drive/domain/member/Member.java | 11 ++++++++++- .../member/persistence/MemberJpaEntity.java | 14 ++++++++++++++ 7 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java index 31cc36b7..4fd30e39 100644 --- a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java @@ -29,9 +29,11 @@ public void execute(DeleteFileInput input) { final MemberID ownerId = MemberID.of(input.ownerId()); final FileID fileId = FileID.of(input.fileId()); - memberGateway.findById(ownerId) + final Member member = memberGateway.findById(ownerId) .orElseThrow(() -> NotFoundException.with(Member.class, input.ownerId())); + member.hasSystemAccess(); + final File file = fileGateway.findById(fileId) .orElseThrow(() -> NotFoundException.with(File.class, input.fileId().toString())); diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java index e5191924..21b46ab9 100644 --- a/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java @@ -29,6 +29,7 @@ public void execute(final SynchronizeMemberInput input) { nickname, null, null, + input.hasSystemAccess(), input.createdAt(), input.updatedAt(), input.synchronizedVersion()); diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java index 2f33d191..27b0c094 100644 --- a/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java @@ -6,6 +6,7 @@ public record SynchronizeMemberInput( String id, String username, String nickname, + Boolean hasSystemAccess, Instant createdAt, Instant updatedAt, Long synchronizedVersion) { @@ -14,10 +15,11 @@ public static SynchronizeMemberInput from( final String id, final String username, final String nickname, + final Boolean hasSystemAccess, final Instant createdAt, final Instant updatedAt, final Long synchronizedVersion) { - return new SynchronizeMemberInput(id, username, nickname, createdAt, updatedAt, synchronizedVersion); + return new SynchronizeMemberInput(id, username, nickname, hasSystemAccess, createdAt, updatedAt, synchronizedVersion); } } diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java index a7888dac..5f361432 100644 --- a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -16,6 +16,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -36,7 +38,15 @@ public class DefaultDeleteFileUseCaseTest { @Test void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { - final Member owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java b/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java new file mode 100644 index 00000000..4c7502dc --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java @@ -0,0 +1,17 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +public class NotAllowedException extends SilentDomainException { + + private NotAllowedException(final List errors) { + super( + "The requested action is not allowed", + errors.stream().map(DomainException.Error::with).toList()); + } + + public static NotAllowedException with(final String action) { + final List list = action == null ? List.of() : List.of(action); + return new NotAllowedException(list); + } +} diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index cf4cd3e3..41266441 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -17,6 +17,8 @@ public class Member extends AggregateRoot { private Quota quota; private Optional quotaRequest; + + private boolean hasSystemAccess; private Instant createdAt; private Instant updatedAt; @@ -29,6 +31,7 @@ private Member( final Nickname nickname, final Quota quota, final QuotaRequest quotaRequest, + final boolean hasSystemAccess, final Instant createdAt, final Instant updatedAt, final Long version) { @@ -37,6 +40,7 @@ private Member( this.nickname = nickname; this.quota = quota == null ? Quota.of(0, QuotaUnit.BYTE) : quota; this.quotaRequest = Optional.ofNullable(quotaRequest); + this.hasSystemAccess = hasSystemAccess; this.createdAt = createdAt; this.updatedAt = updatedAt; @@ -49,10 +53,11 @@ public static Member with( final Nickname nickname, final Quota quota, final QuotaRequest quotaRequest, + final boolean hasSystemAccess, final Instant createdAt, final Instant updatedAt, final Long synchronizedVersion) { - return new Member(id, username, nickname, quota, quotaRequest, createdAt, updatedAt, synchronizedVersion); + return new Member(id, username, nickname, quota, quotaRequest, hasSystemAccess, createdAt, updatedAt, synchronizedVersion); } @Override @@ -142,6 +147,10 @@ public Optional getQuotaRequest() { return quotaRequest; } + public boolean hasSystemAccess(){ + return hasSystemAccess; + } + public Instant getCreatedAt() { return createdAt; } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java index c0c547b6..45c9e6fd 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java @@ -42,6 +42,8 @@ public class MemberJpaEntity { private Instant quotaRequestedAt; + private Boolean hasSystemAccess; + private Instant createdAt; private Instant updatedAt; @@ -57,6 +59,7 @@ public MemberJpaEntity( final Long quotaRequestAmmount, final QuotaUnit quotaRequestUnit, final Instant quotaRequestedAt, + final Boolean hasSystemAccess, final Instant createdAt, final Instant updatedAt, final Long synchronizedVersion) { @@ -68,6 +71,7 @@ public MemberJpaEntity( this.quotaRequestAmmount = quotaRequestAmmount; this.quotaRequestUnit = quotaRequestUnit; this.quotaRequestedAt = quotaRequestedAt; + this.hasSystemAccess = hasSystemAccess == null ? false : hasSystemAccess; this.createdAt = createdAt; this.updatedAt = updatedAt; this.synchronizedVersion = synchronizedVersion; @@ -90,6 +94,7 @@ public Member toDomain() { Nickname.of(getNickname()), Quota.of(getQuotaAmmount(), getQuotaUnit()), quotaRequest, + getHasSystemAccess()tHasSystemAccess(), getCreatedAt(), getUpdatedAt(), getSynchronizedVersion()); @@ -105,6 +110,7 @@ public static MemberJpaEntity fromDomain(final Member member) { member.getQuotaRequest().map(QuotaRequest::quota).map(Quota::amount).orElse(null), member.getQuotaRequest().map(QuotaRequest::quota).map(Quota::unit).orElse(null), member.getQuotaRequest().map(QuotaRequest::requesteddAt).orElse(null), + member.hasSystemAccess(), member.getCreatedAt(), member.getUpdatedAt(), member.getSynchronizedVersion()); @@ -174,6 +180,14 @@ public void setQuotaRequestedAt(Instant quotaRequestedAt) { this.quotaRequestedAt = quotaRequestedAt; } + public Boolean getHasSystemAccess() { + return hasSystemAccess; + } + + public void setHasSystemAccess(Boolean hasSystemAccess) { + this.hasSystemAccess = hasSystemAccess; + } + public Instant getCreatedAt() { return createdAt; } From 7eed8c004a302f48ba4894ac43d7829058c975bd Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 9 Jun 2025 15:58:50 -0300 Subject: [PATCH 40/89] feat: implement file deletion with system access check and add deleteById method in FileJPAGateway --- .../file/delete/DefaultDeleteFileUseCase.java | 13 ++++++++----- .../drive/infrastructure/file/FileJPAGateway.java | 5 +++++ .../member/persistence/MemberJpaEntity.java | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java index 4fd30e39..2446871a 100644 --- a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java @@ -1,6 +1,9 @@ package com.callv2.drive.application.file.delete; +import java.util.Objects; + import com.callv2.drive.domain.exception.InternalErrorException; +import com.callv2.drive.domain.exception.NotAllowedException; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.file.File; import com.callv2.drive.domain.file.FileGateway; @@ -10,12 +13,11 @@ import com.callv2.drive.domain.member.MemberID; import com.callv2.drive.domain.storage.StorageService; -import java.util.Objects; - public class DefaultDeleteFileUseCase extends DeleteFileUseCase { private final MemberGateway memberGateway; private final FileGateway fileGateway; private final StorageService storageService; + public DefaultDeleteFileUseCase( final MemberGateway memberGateway, final FileGateway fileGateway, @@ -24,6 +26,7 @@ public DefaultDeleteFileUseCase( this.fileGateway = Objects.requireNonNull(fileGateway); this.storageService = Objects.requireNonNull(storageService); } + @Override public void execute(DeleteFileInput input) { final MemberID ownerId = MemberID.of(input.ownerId()); @@ -32,13 +35,13 @@ public void execute(DeleteFileInput input) { final Member member = memberGateway.findById(ownerId) .orElseThrow(() -> NotFoundException.with(Member.class, input.ownerId())); - member.hasSystemAccess(); + if (!member.hasSystemAccess()) + throw NotAllowedException.with("Member does not have permission to delete files"); final File file = fileGateway.findById(fileId) .orElseThrow(() -> NotFoundException.with(File.class, input.fileId().toString())); - fileGateway.deleteById(file.getId()); - + fileGateway.deleteById(file.getId()); // TODO maybe needs transactional deleteContentFile(file.getContent().location()); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java index 35cb8815..59bda478 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java @@ -81,4 +81,9 @@ public List findByOwner(MemberID ownerId) { .toList(); } + @Override + public void deleteById(final FileID id) { + this.fileRepository.deleteById(id.getValue()); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java index 45c9e6fd..13b8a8c3 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java @@ -94,7 +94,7 @@ public Member toDomain() { Nickname.of(getNickname()), Quota.of(getQuotaAmmount(), getQuotaUnit()), quotaRequest, - getHasSystemAccess()tHasSystemAccess(), + getHasSystemAccess(), getCreatedAt(), getUpdatedAt(), getSynchronizedVersion()); From e48e6eba1a476cc8af8fe0630821dea303fde898 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 9 Jun 2025 16:34:56 -0300 Subject: [PATCH 41/89] feat: add hasSystemAccess field to Member and update related tests --- .../create/DefaultCreateFileUseCaseTest.java | 15 +++- .../delete/DefaultDeleteFileUseCaseTest.java | 70 +++++++++++-------- .../DefaultSynchronizeMemberUseCaseTest.java | 7 ++ .../callv2/drive/domain/member/Member.java | 17 +++-- .../listener/member/MemberSyncEvent.java | 2 + .../listener/member/MemberSyncListener.java | 3 + 6 files changed, 80 insertions(+), 34 deletions(-) diff --git a/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java index f93c2c82..4a555b42 100644 --- a/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java @@ -71,6 +71,7 @@ void givenAValidParams_whenCallsExecute_thenShouldCreateFile() { Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -148,6 +149,7 @@ void givenAnInvalidFolderId_whenCallsExecute_thenShouldThrowNotFoundException() Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -169,7 +171,8 @@ void givenAnInvalidFolderId_whenCallsExecute_thenShouldThrowNotFoundException() final var expectedExceptionMessage = "[Folder] not found."; final var expectedErrorCount = 1; - final var expectedErrorMessage = "[Folder] with id [%s] not found.".formatted(expectedFolderId.getValue()); + final var expectedErrorMessage = "[Folder] with id [%s] not found." + .formatted(expectedFolderId.getValue()); when(memberGateway.findById(any())) .thenReturn(Optional.of(owner)); @@ -216,7 +219,8 @@ void givenAnInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() final var expectedExceptionMessage = "[Member] not found."; final var expectedErrorCount = 1; - final var expectedErrorMessage = "[Member] with id [%s] not found.".formatted(expectedOwnerId.getValue()); + final var expectedErrorMessage = "[Member] with id [%s] not found." + .formatted(expectedOwnerId.getValue()); when(memberGateway.findById(any())) .thenReturn(Optional.empty()); @@ -255,6 +259,7 @@ void givenAValidParamsWithAlreadyExistingFileNameOnSameFolder_whenCallsExecute_t Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -321,6 +326,7 @@ void givenAValidParams_whenCallsExecuteAndFileGatewayCreateThrowsRandomException Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -405,6 +411,7 @@ void givenAValidParams_whenCallsExecuteAndFileGatewayCreateAndContentGatewayDele Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -489,6 +496,7 @@ void givenAValidParams_whenCallsExecuteAndContentGatewayStoreThrowsRandomExcepti Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -551,6 +559,7 @@ void givenAnInvalidFileName_whenCallsExecute_thenShouldThrowValidationException( Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -611,6 +620,7 @@ void givenAValidParams_whenCallsExecuteAndMemberQuotaIsExceeded_thenShouldThrows Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -667,6 +677,7 @@ void givenAnInvalidParamsWithContentTypeNull_whenCallsExecute_thenShouldThrowsVa Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java index 5f361432..6915705f 100644 --- a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -1,24 +1,38 @@ package com.callv2.drive.application.file.delete; -import com.callv2.drive.domain.exception.NotFoundException; -import com.callv2.drive.domain.file.*; -import com.callv2.drive.domain.folder.FolderID; -import com.callv2.drive.domain.member.*; -import com.callv2.drive.domain.storage.StorageService; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Optional; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.time.Instant; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.domain.file.Content; +import com.callv2.drive.domain.file.File; +import com.callv2.drive.domain.file.FileGateway; +import com.callv2.drive.domain.file.FileID; +import com.callv2.drive.domain.file.FileName; +import com.callv2.drive.domain.folder.FolderID; +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; +import com.callv2.drive.domain.member.Quota; +import com.callv2.drive.domain.member.QuotaUnit; +import com.callv2.drive.domain.member.Username; +import com.callv2.drive.domain.storage.StorageService; @ExtendWith(MockitoExtension.class) public class DefaultDeleteFileUseCaseTest { @@ -44,6 +58,7 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), null, + true, Instant.now(), Instant.now(), 0L) @@ -57,11 +72,9 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { final Content expectedContent = Content.of( "file.txt", "text/plain", - 100L - ); + 100L); final Instant expectedCreatedAt = Instant.now().minus( - java.time.Duration.ofDays(1) - ); + java.time.Duration.ofDays(1)); final Instant expectedUpdatedAt = Instant.now(); final var file = File.with( expectedFileId, @@ -70,8 +83,7 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { expectedFileName, expectedContent, expectedCreatedAt, - expectedUpdatedAt - ); + expectedUpdatedAt); when(memberGateway.findById(expectedOwnerId)) .thenReturn(Optional.of(owner)); @@ -81,8 +93,7 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { final DeleteFileInput input = DeleteFileInput.of( expectedOwnerId.getValue(), - expectedFileId.getValue() - ); + expectedFileId.getValue()); useCase.execute(input); @@ -102,20 +113,25 @@ void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { final MemberID expectedOwnerId = MemberID.of("owner"); final FileID expectedFileId = FileID.unique(); - final String expectedExceptionMessage = "Member with id '%s' not found" - .formatted(expectedOwnerId.getValue()); + final String expectedExceptionMessage = "[Member] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Member] with id [%s] not found.".formatted(expectedOwnerId.getValue()); + + when(memberGateway.findById(any())) + .thenReturn(Optional.empty()); when(memberGateway.findById(expectedOwnerId)) .thenReturn(Optional.empty()); final DeleteFileInput input = DeleteFileInput.of( expectedOwnerId.getValue(), - expectedFileId.getValue() - ); + expectedFileId.getValue()); final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); @@ -125,10 +141,8 @@ void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { } @Test - void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException(){ + void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { } - - } diff --git a/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java index 93757e69..c94c6a18 100644 --- a/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.time.Instant; import java.util.Optional; import org.junit.jupiter.api.Test; @@ -49,6 +50,7 @@ void givenAnValidInput_whenCallsExecute_thenShouldSynchonizeMember() { final var expectedUsername = Username.of(expectedUsernameValue); final var expectedNickname = Nickname.of(expectedNicknameValue); final var expectedQuota = Quota.of(10L, QuotaUnit.GIGABYTE); + final var expectedHasSystemAccess = true; final var oldMember = Member.with( expectedMemberId, @@ -56,6 +58,7 @@ void givenAnValidInput_whenCallsExecute_thenShouldSynchonizeMember() { expectedNickname, expectedQuota, null, + false, expectedCreatedAt, expectedCreatedAt, 0L); @@ -70,6 +73,7 @@ void givenAnValidInput_whenCallsExecute_thenShouldSynchonizeMember() { expectedIdVAlue, expectedUsernameValue, expectedNicknameValue, + expectedHasSystemAccess, expectedCreatedAt, expectedUpdatedAt, expectedSynchronizedVersion); @@ -84,6 +88,7 @@ void givenAnValidInput_whenCallsExecute_thenShouldSynchonizeMember() { assertEquals(expectedUsername, member.getUsername()); assertEquals(expectedNickname, member.getNickname()); assertEquals(expectedQuota, member.getQuota()); + assertEquals(expectedHasSystemAccess, member.hasSystemAccess()); assertEquals(expectedCreatedAt, member.getCreatedAt()); assertEquals(expectedUpdatedAt, member.getUpdatedAt()); assertEquals(expectedSynchronizedVersion, member.getSynchronizedVersion()); @@ -108,6 +113,7 @@ void givenAnValidInputWhitNonExistentMember_whenCallsExecute_thenShouldCreateMem final var expectedUsername = Username.of(expectedUsernameValue); final var expectedNickname = Nickname.of(expectedNicknameValue); final var expectedQuota = Quota.of(0L, QuotaUnit.BYTE); + final var expectedHasSystemAccess = false; when(memberGateway.findById(expectedMemberId)) .thenReturn(Optional.empty()); @@ -119,6 +125,7 @@ void givenAnValidInputWhitNonExistentMember_whenCallsExecute_thenShouldCreateMem expectedIdVAlue, expectedUsernameValue, expectedNicknameValue, + expectedHasSystemAccess, expectedCreatedAt, expectedUpdatedAt, expectedSynchronizedVersion); diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index 41266441..ff437a44 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -17,7 +17,7 @@ public class Member extends AggregateRoot { private Quota quota; private Optional quotaRequest; - + private boolean hasSystemAccess; private Instant createdAt; @@ -57,7 +57,16 @@ public static Member with( final Instant createdAt, final Instant updatedAt, final Long synchronizedVersion) { - return new Member(id, username, nickname, quota, quotaRequest, hasSystemAccess, createdAt, updatedAt, synchronizedVersion); + return new Member( + id, + username, + nickname, + quota, + quotaRequest, + hasSystemAccess, + createdAt, + updatedAt, + synchronizedVersion); } @Override @@ -81,7 +90,7 @@ public Member synchronize(final Member member) { this.nickname = member.nickname; this.username = member.username; - + this.hasSystemAccess = member.hasSystemAccess; this.createdAt = member.createdAt; this.updatedAt = member.updatedAt; this.synchronizedVersion = member.synchronizedVersion; @@ -147,7 +156,7 @@ public Optional getQuotaRequest() { return quotaRequest; } - public boolean hasSystemAccess(){ + public boolean hasSystemAccess() { return hasSystemAccess; } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java index 3c17f289..fd4f296e 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.time.Instant; +import java.util.Set; public record MemberSyncEvent( String id, @@ -15,6 +16,7 @@ public record Data( String email, String nickname, boolean isActive, + Set systems, Instant createdAt, Instant updatedAt, Long synchronizedVersion) implements Serializable { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java index b48fcdcf..12ba39a1 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java @@ -32,10 +32,13 @@ public void handle(final MemberSyncEvent data) { final MemberSyncEvent.Data eventData = data.data(); + final Boolean hasSystemAccess = eventData.systems() != null && eventData.systems().contains("DRIVE"); + final SynchronizeMemberInput createMemberInput = SynchronizeMemberInput.from( eventData.id(), eventData.username(), eventData.nickname(), + hasSystemAccess, eventData.createdAt(), eventData.updatedAt(), eventData.synchronizedVersion()); From 80c62321e1ef36da54d23008dcd64c7635450ead Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 09:16:14 -0300 Subject: [PATCH 42/89] style: remove unnecessary blank line in DefaultDeleteFileUseCase --- .../drive/application/file/delete/DefaultDeleteFileUseCase.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java index 2446871a..41956f9d 100644 --- a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java @@ -14,6 +14,7 @@ import com.callv2.drive.domain.storage.StorageService; public class DefaultDeleteFileUseCase extends DeleteFileUseCase { + private final MemberGateway memberGateway; private final FileGateway fileGateway; private final StorageService storageService; @@ -52,4 +53,5 @@ private void deleteContentFile(final String contentLocation) { throw InternalErrorException.with("Could not delete BinaryContent", e); } } + } From 1cc1b046dfec468bb1053152fce0e6923b2393ba Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 10:09:14 -0300 Subject: [PATCH 43/89] refactor: rename ownerId to deleterId in DeleteFileInput and update execute method in DefaultDeleteFileUseCase --- .../application/file/delete/DefaultDeleteFileUseCase.java | 6 +++--- .../drive/application/file/delete/DeleteFileInput.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java index 41956f9d..11771d1f 100644 --- a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java @@ -29,12 +29,12 @@ public DefaultDeleteFileUseCase( } @Override - public void execute(DeleteFileInput input) { - final MemberID ownerId = MemberID.of(input.ownerId()); + public void execute(final DeleteFileInput input) { + final MemberID ownerId = MemberID.of(input.deleterId()); final FileID fileId = FileID.of(input.fileId()); final Member member = memberGateway.findById(ownerId) - .orElseThrow(() -> NotFoundException.with(Member.class, input.ownerId())); + .orElseThrow(() -> NotFoundException.with(Member.class, input.deleterId())); if (!member.hasSystemAccess()) throw NotAllowedException.with("Member does not have permission to delete files"); diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java index 9b227560..2d821985 100644 --- a/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java @@ -2,9 +2,9 @@ import java.util.UUID; -public record DeleteFileInput(String ownerId, UUID fileId) { +public record DeleteFileInput(String deleterId, UUID fileId) { - public static DeleteFileInput of(final String ownerId, final UUID fileId) { - return new DeleteFileInput(ownerId, fileId); + public static DeleteFileInput of(final String deleterId, final UUID fileId) { + return new DeleteFileInput(deleterId, fileId); } } From c6eb38417577f32bc14662386e4ea8f4ad4a7276 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 10:10:01 -0300 Subject: [PATCH 44/89] style: adjust indentation in DeleteFileInput for consistency --- .../drive/application/file/delete/DeleteFileInput.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java index 2d821985..d14afe18 100644 --- a/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java @@ -4,7 +4,8 @@ public record DeleteFileInput(String deleterId, UUID fileId) { - public static DeleteFileInput of(final String deleterId, final UUID fileId) { - return new DeleteFileInput(deleterId, fileId); - } + public static DeleteFileInput of(final String deleterId, final UUID fileId) { + return new DeleteFileInput(deleterId, fileId); + } + } From 0429b558d2dada8afc1f6b5c5669a47cb4751e58 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 10:13:59 -0300 Subject: [PATCH 45/89] test: add unit test for handling NotFoundException when file ID is invalid --- .../delete/DefaultDeleteFileUseCaseTest.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java index 6915705f..f52e6d70 100644 --- a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -143,6 +143,49 @@ void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { @Test void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { + final MemberID expectedOwnerId = MemberID.of("owner"); + final FileID expectedFileId = FileID.unique(); + + final String expectedExceptionMessage = "[File] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[File] with id [%s] not found.".formatted(expectedFileId.getValue()); + + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); + + when(memberGateway.findById(expectedOwnerId)) + .thenReturn(Optional.of(owner)); + + when(fileGateway.findById(expectedFileId)) + .thenReturn(Optional.empty()); + + final var input = DeleteFileInput.of( + expectedOwnerId.getValue(), + expectedFileId.getValue()); + + final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + + verify(memberGateway, times(1)).findById(any()); + verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(fileGateway, times(1)).findById(any()); + verify(fileGateway, times(1)).findById(eq(expectedFileId)); + verify(fileGateway, never()).deleteById(any()); + verify(storageService, never()).delete(any()); + } } From 973d802073f8c3415facefbc69048c2045e3cac0 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 10:24:12 -0300 Subject: [PATCH 46/89] fix: fix NotAllowedException messages for consistency --- .../drive/application/file/delete/DefaultDeleteFileUseCase.java | 2 +- .../com/callv2/drive/domain/exception/NotAllowedException.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java index 11771d1f..a5279c75 100644 --- a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java @@ -37,7 +37,7 @@ public void execute(final DeleteFileInput input) { .orElseThrow(() -> NotFoundException.with(Member.class, input.deleterId())); if (!member.hasSystemAccess()) - throw NotAllowedException.with("Member does not have permission to delete files"); + throw NotAllowedException.with("Member does not have permission to delete files."); final File file = fileGateway.findById(fileId) .orElseThrow(() -> NotFoundException.with(File.class, input.fileId().toString())); diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java b/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java index 4c7502dc..dd829afd 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java @@ -6,7 +6,7 @@ public class NotAllowedException extends SilentDomainException { private NotAllowedException(final List errors) { super( - "The requested action is not allowed", + "The requested action is not allowed.", errors.stream().map(DomainException.Error::with).toList()); } From bf7af9e9b0be52acffd0b3646e3a078693307e2f Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 10:24:24 -0300 Subject: [PATCH 47/89] test: add unit test for handling NotAllowedException when member lacks permission to delete files --- .../delete/DefaultDeleteFileUseCaseTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java index f52e6d70..743e55bf 100644 --- a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -12,12 +12,14 @@ import java.time.Instant; import java.util.Optional; +import org.checkerframework.checker.units.qual.A; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import com.callv2.drive.domain.exception.NotAllowedException; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.file.Content; import com.callv2.drive.domain.file.File; @@ -188,4 +190,48 @@ void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { } + @Test + void givenAValidMemberIdButNotHaveSystemAccess_whenCallsExecute_thenShouldThrowNotFoundException() { + + final MemberID expectedOwnerId = MemberID.of("owner"); + final FileID expectedFileId = FileID.unique(); + + final String expectedExceptionMessage = "The requested action is not allowed."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "Member does not have permission to delete files."; + + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + false, + Instant.now(), + Instant.now(), + 0L) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); + + when(memberGateway.findById(expectedOwnerId)) + .thenReturn(Optional.of(owner)); + + final var input = DeleteFileInput.of( + expectedOwnerId.getValue(), + expectedFileId.getValue()); + + final var actualException = assertThrows(NotAllowedException.class, () -> useCase.execute(input)); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + + verify(memberGateway, times(1)).findById(any()); + verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(fileGateway, never()).findById(any()); + verify(fileGateway, never()).deleteById(any()); + verify(storageService, never()).delete(any()); + + } + } From d603adf4e7a9301781f9d8fc2f737c7f78bed497 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 10:39:40 -0300 Subject: [PATCH 48/89] refactor: rename ownerId to deleterId in DefaultDeleteFileUseCaseTest for clarity --- .../delete/DefaultDeleteFileUseCaseTest.java | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java index 743e55bf..74050c28 100644 --- a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -54,7 +54,7 @@ public class DefaultDeleteFileUseCaseTest { @Test void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { - final var owner = Member.with( + final var deleter = Member.with( MemberID.of("owner"), Username.of("username"), Nickname.of("nickname"), @@ -68,7 +68,7 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { .approveQuotaRequest(); final FileID expectedFileId = FileID.unique(); - final MemberID expectedOwnerId = owner.getId(); + final MemberID expectedDeleterId = deleter.getId(); final FolderID expectedFolderId = FolderID.unique(); final FileName expectedFileName = FileName.of("file.txt"); final Content expectedContent = Content.of( @@ -80,27 +80,27 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { final Instant expectedUpdatedAt = Instant.now(); final var file = File.with( expectedFileId, - expectedOwnerId, + expectedDeleterId, expectedFolderId, expectedFileName, expectedContent, expectedCreatedAt, expectedUpdatedAt); - when(memberGateway.findById(expectedOwnerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); when(fileGateway.findById(expectedFileId)) .thenReturn(Optional.of(file)); final DeleteFileInput input = DeleteFileInput.of( - expectedOwnerId.getValue(), + expectedDeleterId.getValue(), expectedFileId.getValue()); useCase.execute(input); verify(memberGateway, times(1)).findById(any()); - verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); verify(fileGateway, times(1)).findById(any()); verify(fileGateway, times(1)).findById(eq(expectedFileId)); verify(fileGateway, times(1)).deleteById(any()); @@ -112,21 +112,21 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { @Test void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { - final MemberID expectedOwnerId = MemberID.of("owner"); + final MemberID expectedDeleterId = MemberID.of("deleter"); final FileID expectedFileId = FileID.unique(); final String expectedExceptionMessage = "[Member] not found."; final var expectedErrorCount = 1; - final var expectedErrorMessage = "[Member] with id [%s] not found.".formatted(expectedOwnerId.getValue()); + final var expectedErrorMessage = "[Member] with id [%s] not found.".formatted(expectedDeleterId.getValue()); when(memberGateway.findById(any())) .thenReturn(Optional.empty()); - when(memberGateway.findById(expectedOwnerId)) + when(memberGateway.findById(expectedDeleterId)) .thenReturn(Optional.empty()); final DeleteFileInput input = DeleteFileInput.of( - expectedOwnerId.getValue(), + expectedDeleterId.getValue(), expectedFileId.getValue()); final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); @@ -136,7 +136,7 @@ void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); - verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); verify(fileGateway, never()).findById(any()); verify(fileGateway, never()).deleteById(any()); verify(storageService, never()).delete(any()); @@ -145,14 +145,14 @@ void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { @Test void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { - final MemberID expectedOwnerId = MemberID.of("owner"); + final MemberID expectedDeleterId = MemberID.of("deleter"); final FileID expectedFileId = FileID.unique(); final String expectedExceptionMessage = "[File] not found."; final var expectedErrorCount = 1; final var expectedErrorMessage = "[File] with id [%s] not found.".formatted(expectedFileId.getValue()); - final var owner = Member.with( + final var deleter = Member.with( MemberID.of("owner"), Username.of("username"), Nickname.of("nickname"), @@ -165,14 +165,14 @@ void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); - when(memberGateway.findById(expectedOwnerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); when(fileGateway.findById(expectedFileId)) .thenReturn(Optional.empty()); final var input = DeleteFileInput.of( - expectedOwnerId.getValue(), + expectedDeleterId.getValue(), expectedFileId.getValue()); final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); @@ -182,7 +182,7 @@ void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); - verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); verify(fileGateway, times(1)).findById(any()); verify(fileGateway, times(1)).findById(eq(expectedFileId)); verify(fileGateway, never()).deleteById(any()); @@ -193,14 +193,14 @@ void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { @Test void givenAValidMemberIdButNotHaveSystemAccess_whenCallsExecute_thenShouldThrowNotFoundException() { - final MemberID expectedOwnerId = MemberID.of("owner"); + final MemberID expectedDeleterId = MemberID.of("deleter"); final FileID expectedFileId = FileID.unique(); final String expectedExceptionMessage = "The requested action is not allowed."; final var expectedErrorCount = 1; final var expectedErrorMessage = "Member does not have permission to delete files."; - final var owner = Member.with( + final var deleter = Member.with( MemberID.of("owner"), Username.of("username"), Nickname.of("nickname"), @@ -213,11 +213,11 @@ void givenAValidMemberIdButNotHaveSystemAccess_whenCallsExecute_thenShouldThrowN .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); - when(memberGateway.findById(expectedOwnerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); final var input = DeleteFileInput.of( - expectedOwnerId.getValue(), + expectedDeleterId.getValue(), expectedFileId.getValue()); final var actualException = assertThrows(NotAllowedException.class, () -> useCase.execute(input)); @@ -227,7 +227,7 @@ void givenAValidMemberIdButNotHaveSystemAccess_whenCallsExecute_thenShouldThrowN assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); - verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); verify(fileGateway, never()).findById(any()); verify(fileGateway, never()).deleteById(any()); verify(storageService, never()).delete(any()); From 99625bf010c684835a65004ff37546533ae9fe0a Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 10:39:40 -0300 Subject: [PATCH 49/89] refactor: rename ownerId to deleterId in DefaultDeleteFileUseCaseTest for clarity --- .../delete/DefaultDeleteFileUseCaseTest.java | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java index 743e55bf..a0559cbe 100644 --- a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -54,8 +54,8 @@ public class DefaultDeleteFileUseCaseTest { @Test void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { - final var owner = Member.with( - MemberID.of("owner"), + final var deleter = Member.with( + MemberID.of("deleter"), Username.of("username"), Nickname.of("nickname"), Quota.of(0, QuotaUnit.BYTE), @@ -68,7 +68,7 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { .approveQuotaRequest(); final FileID expectedFileId = FileID.unique(); - final MemberID expectedOwnerId = owner.getId(); + final MemberID expectedDeleterId = deleter.getId(); final FolderID expectedFolderId = FolderID.unique(); final FileName expectedFileName = FileName.of("file.txt"); final Content expectedContent = Content.of( @@ -80,27 +80,27 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { final Instant expectedUpdatedAt = Instant.now(); final var file = File.with( expectedFileId, - expectedOwnerId, + expectedDeleterId, expectedFolderId, expectedFileName, expectedContent, expectedCreatedAt, expectedUpdatedAt); - when(memberGateway.findById(expectedOwnerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); when(fileGateway.findById(expectedFileId)) .thenReturn(Optional.of(file)); final DeleteFileInput input = DeleteFileInput.of( - expectedOwnerId.getValue(), + expectedDeleterId.getValue(), expectedFileId.getValue()); useCase.execute(input); verify(memberGateway, times(1)).findById(any()); - verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); verify(fileGateway, times(1)).findById(any()); verify(fileGateway, times(1)).findById(eq(expectedFileId)); verify(fileGateway, times(1)).deleteById(any()); @@ -112,21 +112,21 @@ void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { @Test void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { - final MemberID expectedOwnerId = MemberID.of("owner"); + final MemberID expectedDeleterId = MemberID.of("deleter"); final FileID expectedFileId = FileID.unique(); final String expectedExceptionMessage = "[Member] not found."; final var expectedErrorCount = 1; - final var expectedErrorMessage = "[Member] with id [%s] not found.".formatted(expectedOwnerId.getValue()); + final var expectedErrorMessage = "[Member] with id [%s] not found.".formatted(expectedDeleterId.getValue()); when(memberGateway.findById(any())) .thenReturn(Optional.empty()); - when(memberGateway.findById(expectedOwnerId)) + when(memberGateway.findById(expectedDeleterId)) .thenReturn(Optional.empty()); final DeleteFileInput input = DeleteFileInput.of( - expectedOwnerId.getValue(), + expectedDeleterId.getValue(), expectedFileId.getValue()); final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); @@ -136,7 +136,7 @@ void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); - verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); verify(fileGateway, never()).findById(any()); verify(fileGateway, never()).deleteById(any()); verify(storageService, never()).delete(any()); @@ -145,14 +145,14 @@ void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { @Test void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { - final MemberID expectedOwnerId = MemberID.of("owner"); + final MemberID expectedDeleterId = MemberID.of("deleter"); final FileID expectedFileId = FileID.unique(); final String expectedExceptionMessage = "[File] not found."; final var expectedErrorCount = 1; final var expectedErrorMessage = "[File] with id [%s] not found.".formatted(expectedFileId.getValue()); - final var owner = Member.with( + final var deleter = Member.with( MemberID.of("owner"), Username.of("username"), Nickname.of("nickname"), @@ -165,14 +165,14 @@ void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); - when(memberGateway.findById(expectedOwnerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); when(fileGateway.findById(expectedFileId)) .thenReturn(Optional.empty()); final var input = DeleteFileInput.of( - expectedOwnerId.getValue(), + expectedDeleterId.getValue(), expectedFileId.getValue()); final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); @@ -182,7 +182,7 @@ void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); - verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); verify(fileGateway, times(1)).findById(any()); verify(fileGateway, times(1)).findById(eq(expectedFileId)); verify(fileGateway, never()).deleteById(any()); @@ -193,14 +193,14 @@ void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { @Test void givenAValidMemberIdButNotHaveSystemAccess_whenCallsExecute_thenShouldThrowNotFoundException() { - final MemberID expectedOwnerId = MemberID.of("owner"); + final MemberID expectedDeleterId = MemberID.of("deleter"); final FileID expectedFileId = FileID.unique(); final String expectedExceptionMessage = "The requested action is not allowed."; final var expectedErrorCount = 1; final var expectedErrorMessage = "Member does not have permission to delete files."; - final var owner = Member.with( + final var deleter = Member.with( MemberID.of("owner"), Username.of("username"), Nickname.of("nickname"), @@ -213,11 +213,11 @@ void givenAValidMemberIdButNotHaveSystemAccess_whenCallsExecute_thenShouldThrowN .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); - when(memberGateway.findById(expectedOwnerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); final var input = DeleteFileInput.of( - expectedOwnerId.getValue(), + expectedDeleterId.getValue(), expectedFileId.getValue()); final var actualException = assertThrows(NotAllowedException.class, () -> useCase.execute(input)); @@ -227,7 +227,7 @@ void givenAValidMemberIdButNotHaveSystemAccess_whenCallsExecute_thenShouldThrowN assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); - verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); verify(fileGateway, never()).findById(any()); verify(fileGateway, never()).deleteById(any()); verify(storageService, never()).delete(any()); From a93b27ddd79215d7fe26561bf68519ef28b38d0e Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 11:39:51 -0300 Subject: [PATCH 50/89] refactor: improve folder name validation and update method parameters for clarity --- .../update/name/DefaultUpdateFolderNameUseCase.java | 8 ++++---- .../folder/update/name/UpdateFolderNameInput.java | 4 ++++ .../infrastructure/api/controller/FolderController.java | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java index 9f5d9604..bbdb4fd2 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java @@ -6,7 +6,7 @@ import com.callv2.drive.domain.folder.FolderGateway; import com.callv2.drive.domain.folder.FolderID; import com.callv2.drive.domain.folder.FolderName; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; public class DefaultUpdateFolderNameUseCase extends UpdateFolderNameUseCase { @@ -35,14 +35,14 @@ public void execute(final UpdateFolderNameInput input) { this.folderGateway.update(folder.changeName(folderName)); } - private void validateFolderName(Folder parentFolder, FolderName name) { + private void validateFolderName(final Folder parentFolder, final FolderName name) { final Notification notification = Notification.create(); if (parentFolder.getSubFolders().stream().anyMatch(subFolder -> subFolder.name().equals(name))) - notification.append(Error.with("Folder with the same name already exists")); + notification.append(ValidationError.with("Folder with the same name already exists")); if (notification.hasError()) - throw ValidationException.with("Could not update folder name Aggregate Folder", notification); + throw ValidationException.with("Could not update folder name", notification); } } \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java index 9531c6d6..f590bf3b 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java @@ -6,4 +6,8 @@ public record UpdateFolderNameInput( UUID folderId, String name) { + public static UpdateFolderNameInput of(final UUID folderId, final String name) { + return new UpdateFolderNameInput(folderId, name); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java index d2f25dbf..7be0b3c3 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java @@ -110,9 +110,9 @@ public ResponseEntity> list( } @Override - public ResponseEntity changeName(UUID id, String request) { + public ResponseEntity changeName(UUID id, String newName) { - this.updateFolderNameUseCase.execute(new UpdateFolderNameInput(id, request)); + this.updateFolderNameUseCase.execute(new UpdateFolderNameInput(id, newName)); return ResponseEntity.noContent().build(); } From c86d5d1fc524f779914f7188365956f61eb15c9b Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Tue, 10 Jun 2025 11:41:44 -0300 Subject: [PATCH 51/89] feat: add operation for changing folder name with appropriate responses --- .../com/callv2/drive/infrastructure/api/FolderAPI.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java index faa2c4c6..e9e406b7 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java @@ -90,6 +90,13 @@ ResponseEntity> list( @RequestParam(name = "filters", required = false) List filters); @PatchMapping(value = "{id}/change-name", consumes = { MediaType.APPLICATION_JSON_VALUE }) + @Operation(summary = "Change folder name", description = "This method changes the name of a folder", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "Folder name changed successfully", content = @Content(schema = @Schema(implementation = Void.class))), + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), + @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + }) ResponseEntity changeName(@PathVariable(required = true) UUID id, @RequestBody String request); } From 2ad7d97685a4b9446eee87176321aa0e58c92c02 Mon Sep 17 00:00:00 2001 From: Artur Lampert Date: Tue, 10 Jun 2025 16:12:27 -0300 Subject: [PATCH 52/89] feat: add delete file functionality with API endpoint and use case integration --- .../callv2/drive/infrastructure/api/FileAPI.java | 10 ++++++++++ .../api/controller/FileController.java | 15 +++++++++++++++ .../configuration/usecase/FileUseCaseConfig.java | 7 +++++++ infrastructure/src/main/resources/application.yml | 2 +- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java index bd0cd9ab..91ff25d7 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java @@ -6,6 +6,7 @@ import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -47,6 +48,15 @@ ResponseEntity create( @PathVariable(required = true, name = "folderId") UUID folderId, @RequestPart("file") MultipartFile file); + @DeleteMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) + @Operation(summary = "Delete a file", description = "This method deletes a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "File deleted successfully"), + @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + }) + ResponseEntity delete(@PathVariable(required = true) UUID id); + @GetMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a file", description = "This method retrive a file", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses({ diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java index 5fb9edb0..e7a6db47 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java @@ -17,6 +17,8 @@ import com.callv2.drive.application.file.content.get.GetFileContentOutput; import com.callv2.drive.application.file.content.get.GetFileContentUseCase; import com.callv2.drive.application.file.create.CreateFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileInput; import com.callv2.drive.application.file.retrieve.get.GetFileInput; import com.callv2.drive.application.file.retrieve.get.GetFileUseCase; import com.callv2.drive.application.file.retrieve.list.ListFilesUseCase; @@ -37,16 +39,19 @@ public class FileController implements FileAPI { private final CreateFileUseCase createFileUseCase; + private final DeleteFileUseCase deleteFileUseCase; private final GetFileUseCase getFileUseCase; private final GetFileContentUseCase getFileContentUseCase; private final ListFilesUseCase listFilesUseCase; public FileController( final CreateFileUseCase createFileUseCase, + final DeleteFileUseCase deleteFileUseCase, final GetFileUseCase getFileUseCase, final GetFileContentUseCase getFileContentUseCase, final ListFilesUseCase listFilesUseCase) { this.createFileUseCase = createFileUseCase; + this.deleteFileUseCase = deleteFileUseCase; this.getFileUseCase = getFileUseCase; this.getFileContentUseCase = getFileContentUseCase; this.listFilesUseCase = listFilesUseCase; @@ -65,6 +70,16 @@ public ResponseEntity create(UUID folderId, MultipartFile fi .body(response); } + @Override + public ResponseEntity delete(UUID id) { + final var deleterId = SecurityContext.getAuthenticatedUser(); + + DeleteFileInput deleteFileInput = DeleteFileInput.of(deleterId, id); + + deleteFileUseCase.execute(deleteFileInput); + return ResponseEntity.noContent().build(); + } + @Override public ResponseEntity getById(UUID id) { return ResponseEntity.ok(FilePresenter.present(getFileUseCase.execute(GetFileInput.from(id)))); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java index 1906d318..030eb267 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java @@ -7,6 +7,8 @@ import com.callv2.drive.application.file.content.get.GetFileContentUseCase; import com.callv2.drive.application.file.create.CreateFileUseCase; import com.callv2.drive.application.file.create.DefaultCreateFileUseCase; +import com.callv2.drive.application.file.delete.DefaultDeleteFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileUseCase; import com.callv2.drive.application.file.retrieve.get.DefaultGetFileUseCase; import com.callv2.drive.application.file.retrieve.get.GetFileUseCase; import com.callv2.drive.application.file.retrieve.list.DefaultListFilesUseCase; @@ -40,6 +42,11 @@ CreateFileUseCase createFileUseCase() { return new DefaultCreateFileUseCase(memberGateway, folderGateway, fileGateway, storageService); } + @Bean + DeleteFileUseCase deleteFileUseCase() { + return new DefaultDeleteFileUseCase(memberGateway, fileGateway, storageService); + } + @Bean GetFileUseCase getFileUseCase() { return new DefaultGetFileUseCase(fileGateway); diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index e2867587..473422ee 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -46,7 +46,7 @@ spring: max-interval: 8000 multiplier: 2 default-requeue-rejected: false - virtual-host: ${rabbitmq.virtual-host} + # virtual-host: ${rabbitmq.virtual-host} dynamic: ${rabbitmq.auto-create} host: ${rabbitmq.host} port: ${rabbitmq.port} From 26d0aacdde74e1149934fea441e96553d53fed5a Mon Sep 17 00:00:00 2001 From: Artur Lampert Date: Tue, 10 Jun 2025 16:35:31 -0300 Subject: [PATCH 53/89] feat: add delete file functionality with API endpoint and use case integration --- .../callv2/drive/infrastructure/api/FileAPI.java | 10 ++++++++++ .../api/controller/FileController.java | 15 +++++++++++++++ .../configuration/usecase/FileUseCaseConfig.java | 7 +++++++ infrastructure/src/main/resources/application.yml | 1 - 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java index bd0cd9ab..91ff25d7 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java @@ -6,6 +6,7 @@ import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -47,6 +48,15 @@ ResponseEntity create( @PathVariable(required = true, name = "folderId") UUID folderId, @RequestPart("file") MultipartFile file); + @DeleteMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) + @Operation(summary = "Delete a file", description = "This method deletes a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "File deleted successfully"), + @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + }) + ResponseEntity delete(@PathVariable(required = true) UUID id); + @GetMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a file", description = "This method retrive a file", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses({ diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java index 5fb9edb0..e7a6db47 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java @@ -17,6 +17,8 @@ import com.callv2.drive.application.file.content.get.GetFileContentOutput; import com.callv2.drive.application.file.content.get.GetFileContentUseCase; import com.callv2.drive.application.file.create.CreateFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileInput; import com.callv2.drive.application.file.retrieve.get.GetFileInput; import com.callv2.drive.application.file.retrieve.get.GetFileUseCase; import com.callv2.drive.application.file.retrieve.list.ListFilesUseCase; @@ -37,16 +39,19 @@ public class FileController implements FileAPI { private final CreateFileUseCase createFileUseCase; + private final DeleteFileUseCase deleteFileUseCase; private final GetFileUseCase getFileUseCase; private final GetFileContentUseCase getFileContentUseCase; private final ListFilesUseCase listFilesUseCase; public FileController( final CreateFileUseCase createFileUseCase, + final DeleteFileUseCase deleteFileUseCase, final GetFileUseCase getFileUseCase, final GetFileContentUseCase getFileContentUseCase, final ListFilesUseCase listFilesUseCase) { this.createFileUseCase = createFileUseCase; + this.deleteFileUseCase = deleteFileUseCase; this.getFileUseCase = getFileUseCase; this.getFileContentUseCase = getFileContentUseCase; this.listFilesUseCase = listFilesUseCase; @@ -65,6 +70,16 @@ public ResponseEntity create(UUID folderId, MultipartFile fi .body(response); } + @Override + public ResponseEntity delete(UUID id) { + final var deleterId = SecurityContext.getAuthenticatedUser(); + + DeleteFileInput deleteFileInput = DeleteFileInput.of(deleterId, id); + + deleteFileUseCase.execute(deleteFileInput); + return ResponseEntity.noContent().build(); + } + @Override public ResponseEntity getById(UUID id) { return ResponseEntity.ok(FilePresenter.present(getFileUseCase.execute(GetFileInput.from(id)))); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java index 1906d318..030eb267 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java @@ -7,6 +7,8 @@ import com.callv2.drive.application.file.content.get.GetFileContentUseCase; import com.callv2.drive.application.file.create.CreateFileUseCase; import com.callv2.drive.application.file.create.DefaultCreateFileUseCase; +import com.callv2.drive.application.file.delete.DefaultDeleteFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileUseCase; import com.callv2.drive.application.file.retrieve.get.DefaultGetFileUseCase; import com.callv2.drive.application.file.retrieve.get.GetFileUseCase; import com.callv2.drive.application.file.retrieve.list.DefaultListFilesUseCase; @@ -40,6 +42,11 @@ CreateFileUseCase createFileUseCase() { return new DefaultCreateFileUseCase(memberGateway, folderGateway, fileGateway, storageService); } + @Bean + DeleteFileUseCase deleteFileUseCase() { + return new DefaultDeleteFileUseCase(memberGateway, fileGateway, storageService); + } + @Bean GetFileUseCase getFileUseCase() { return new DefaultGetFileUseCase(fileGateway); diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index e2867587..7bc79f30 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -46,7 +46,6 @@ spring: max-interval: 8000 multiplier: 2 default-requeue-rejected: false - virtual-host: ${rabbitmq.virtual-host} dynamic: ${rabbitmq.auto-create} host: ${rabbitmq.host} port: ${rabbitmq.port} From 536aae982265aaaad2c7d5a27b933651981a869e Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Wed, 11 Jun 2025 09:25:20 -0300 Subject: [PATCH 54/89] fix: add nullable constraint to hasSystemAccess in MemberJpaEntity --- .../drive/infrastructure/member/persistence/MemberJpaEntity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java index 13b8a8c3..ddb197c9 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java @@ -42,6 +42,7 @@ public class MemberJpaEntity { private Instant quotaRequestedAt; + @Column(nullable = false) private Boolean hasSystemAccess; private Instant createdAt; From a226eaf4c60684e5222f9a5945eb5910ed36c9c1 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Wed, 11 Jun 2025 09:49:57 -0300 Subject: [PATCH 55/89] refactor: remove unused imports in test classes --- .../application/file/delete/DefaultDeleteFileUseCaseTest.java | 1 - .../member/synchronize/DefaultSynchronizeMemberUseCaseTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java index a0559cbe..d121b867 100644 --- a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -12,7 +12,6 @@ import java.time.Instant; import java.util.Optional; -import org.checkerframework.checker.units.qual.A; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; diff --git a/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java index c94c6a18..70e250ee 100644 --- a/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java @@ -10,7 +10,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.time.Instant; import java.util.Optional; import org.junit.jupiter.api.Test; From dac6240a019956670f5a476a537c85240a8bbc82 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Wed, 11 Jun 2025 10:18:49 -0300 Subject: [PATCH 56/89] refactor: rename parameter in NotAllowedException constructor for clarity --- .../callv2/drive/domain/exception/NotAllowedException.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java b/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java index dd829afd..ce0db28d 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java @@ -4,10 +4,10 @@ public class NotAllowedException extends SilentDomainException { - private NotAllowedException(final List errors) { + private NotAllowedException(final List actions) { super( "The requested action is not allowed.", - errors.stream().map(DomainException.Error::with).toList()); + actions.stream().map(DomainException.Error::with).toList()); } public static NotAllowedException with(final String action) { From 020579f061198433bcfaf26d50e24ffd971c838c Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Wed, 11 Jun 2025 10:19:33 -0300 Subject: [PATCH 57/89] style: add a new line for code formatting consistency in FileGateway interface --- .../src/main/java/com/callv2/drive/domain/file/FileGateway.java | 1 + 1 file changed, 1 insertion(+) diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java b/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java index f38b9b3d..46064f11 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java @@ -21,4 +21,5 @@ public interface FileGateway { Page findAll(SearchQuery searchQuery); void deleteById(FileID id); + } From 0a0b68b534b001e92dcf6e7f2feca1b5eae7bd89 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Wed, 11 Jun 2025 10:29:23 -0300 Subject: [PATCH 58/89] feat: add NotAllowedException handler to GlobalExceptionHandler --- .../api/controller/GlobalExceptionHandler.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/GlobalExceptionHandler.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/GlobalExceptionHandler.java index 240f60d9..a04c449a 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/GlobalExceptionHandler.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import com.callv2.drive.domain.exception.DomainException; import com.callv2.drive.domain.exception.InternalErrorException; +import com.callv2.drive.domain.exception.NotAllowedException; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.QuotaExceededException; import com.callv2.drive.domain.exception.ValidationException; @@ -17,7 +18,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(value = Throwable.class) public ResponseEntity handle(final Throwable ex) { - return ResponseEntity.internalServerError().body(ApiError.with("Internal Server Error" + ex.getMessage())); + return ResponseEntity.internalServerError().body(ApiError.with("Internal Server Error")); } @ExceptionHandler(value = InternalErrorException.class) @@ -51,4 +52,9 @@ public ResponseEntity handle(final InvalidDataAccessApiUsageException .body(ApiError.with("Invalid Data Access Api Usage [%s]".formatted(ex.getMessage()))); } + @ExceptionHandler(value = NotAllowedException.class) + public ResponseEntity handle(final NotAllowedException ex) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.from(ex)); + } + } From 7e6a01e0d8aa297511e52fa5a94cfa93a87ec46d Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 13 Jun 2025 18:31:40 -0300 Subject: [PATCH 59/89] refactor: remove SubFolder entity Co-authored-by: Artur S --- .../create/DefaultCreateFolderUseCase.java | 8 +- .../folder/move/DefaultMoveFolderUseCase.java | 40 +++++----- .../retrieve/get/DefaultGetFolderUseCase.java | 5 +- .../folder/retrieve/get/GetFolderOutput.java | 26 +++---- .../get/root/DefaultGetRootFolderUseCase.java | 6 +- .../get/root/GetRootFolderOutput.java | 12 ++- .../retrieve/list/FolderListOutput.java | 13 ++-- .../get/DefaultGetFolderUseCaseTest.java | 4 +- .../callv2/drive/domain/folder/Folder.java | 60 +-------------- .../drive/domain/folder/FolderGateway.java | 3 + .../callv2/drive/domain/folder/SubFolder.java | 15 ---- .../folder/FolderJpaGateway.java | 11 +++ .../folder/model/FolderListResponse.java | 8 +- .../folder/persistence/FolderJpaEntity.java | 28 ------- .../persistence/FolderJpaRepository.java | 3 + .../folder/persistence/SubFolderID.java | 77 ------------------- .../persistence/SubFolderJpaEntity.java | 76 ------------------ .../folder/presenter/FolderPresenter.java | 7 +- 18 files changed, 85 insertions(+), 317 deletions(-) delete mode 100644 domain/src/main/java/com/callv2/drive/domain/folder/SubFolder.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderID.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderJpaEntity.java diff --git a/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java index f1de2564..70522841 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java @@ -1,5 +1,7 @@ package com.callv2.drive.application.folder.create; +import java.util.Set; + import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.folder.Folder; @@ -39,8 +41,12 @@ public CreateFolderOutput execute(final CreateFolderInput input) { } private Folder createFolder(final MemberID ownerId, FolderName name, final Folder parentFolder) { + final Notification notification = Notification.create(); - if (parentFolder.getSubFolders().stream().anyMatch(subFolder -> subFolder.name().equals(name))) + + final Set subFolders = folderGateway.findByParentFolderId(parentFolder.getId()); + + if (subFolders.stream().anyMatch(subFolder -> subFolder.getName().equals(name))) notification.append(ValidationError.with("Folder with the same name already exists")); final Folder folder = notification.valdiate(() -> Folder.create(ownerId, name, parentFolder)); diff --git a/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java index b0d2dde2..88d126eb 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java @@ -1,7 +1,7 @@ package com.callv2.drive.application.folder.move; -import java.util.List; import java.util.Objects; +import java.util.Set; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.ValidationException; @@ -9,6 +9,7 @@ import com.callv2.drive.domain.folder.FolderGateway; import com.callv2.drive.domain.folder.FolderID; import com.callv2.drive.domain.validation.ValidationError; +import com.callv2.drive.domain.validation.handler.Notification; public class DefaultMoveFolderUseCase extends MoveFolderUseCase { @@ -27,17 +28,14 @@ public void execute(MoveFolderInput input) { final Folder folder = findFolder(folderId); final Folder newParentFolder = findFolder(newParentFolderId); - // TODO inform the cause - if (!canMove(folder, newParentFolder)) - throw ValidationException.with("Invalid move operation", ValidationError.with("Cannot move folder")); + final Notification notification = Notification.create(); + validateMove(folder, newParentFolder, notification); + if (notification.hasError()) + throw ValidationException.with("Invalid move operation", notification); - final Folder oldParentFolder = findFolder(folder.getParentFolder()); - oldParentFolder.removeSubFolder(folder); - - newParentFolder.addSubFolder(folder); folder.changeParentFolder(newParentFolder); - folderGateway.updateAll(List.of(folder, newParentFolder, oldParentFolder)); + folderGateway.update(folder); } private Folder findFolder(FolderID id) { @@ -46,29 +44,35 @@ private Folder findFolder(FolderID id) { .orElseThrow(() -> NotFoundException.with(Folder.class, id.getValue().toString())); } - private boolean canMove(Folder folder, Folder newParentFolder) { + private void validateMove(final Folder folder, final Folder newParentFolder, final Notification notification) { + + final Set newParentFolderSubFolders = this.folderGateway.findByParentFolderId(newParentFolder.getId()); - if (newParentFolder.getSubFolders().stream().anyMatch(sf -> sf.name().equals(folder.getName()))) - return false; + if (newParentFolderSubFolders.stream().anyMatch(sf -> sf.getName().equals(folder.getName()))) + notification.append( + ValidationError.with("A folder with the same name already exists in the target parent folder.")); if (folder.equals(newParentFolder)) - return false; + notification.append(ValidationError.with("Cannot move a folder into itself.")); if (folder.isRootFolder()) - return false; + notification.append(ValidationError.with("Cannot move the root folder.")); if (newParentFolder.isRootFolder()) - return true; + return; Folder actualParent = newParentFolder; while (!actualParent.isRootFolder()) { - if (folder.equals(actualParent)) - return false; + if (folder.equals(actualParent)) { + notification.append( + ValidationError.with("Cannot move a folder into one of its subfolders.")); + break; + } actualParent = findFolder(actualParent.getParentFolder()); } - return true; + return; } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCase.java index 18668196..222f6f40 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCase.java @@ -28,7 +28,10 @@ public GetFolderOutput execute(GetFolderInput input) { .orElseThrow(() -> NotFoundException.with(Folder.class, input.id().toString())); return GetFolderOutput - .from(folder, fileGateway.findByFolder(folder.getId())); + .from( + folder, + folderGateway.findByParentFolderId(folder.getId()), + fileGateway.findByFolder(folder.getId())); } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java index 4b041eb2..71b1b772 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.List; +import java.util.Set; import java.util.UUID; import com.callv2.drive.domain.folder.Folder; @@ -16,23 +17,20 @@ public record GetFolderOutput( Instant updatedAt, Instant deletedAt) { - public static GetFolderOutput from(final Folder folder, List files) { - - final var subFolders = folder.getSubFolders() != null - ? folder.getSubFolders() - .stream() - .map(GetFolderOutput.SubFolder::from) - .toList() - : null; - - final var filesOutput = files != null ? files.stream().map(GetFolderOutput.File::from).toList() : null; + public static GetFolderOutput from( + final Folder folder, + final Set subFolders, + final List files) { return new GetFolderOutput( folder.getId().getValue(), folder.getName().value(), folder.getParentFolder().getValue(), - subFolders, - filesOutput, + subFolders + .stream() + .map(GetFolderOutput.SubFolder::from) + .toList(), + files.stream().map(GetFolderOutput.File::from).toList(), folder.getCreatedAt(), folder.getUpdatedAt(), folder.getDeletedAt()); @@ -40,8 +38,8 @@ public static GetFolderOutput from(final Folder folder, List root = folderGateway.findRoot(); final Folder folder = root.isPresent() ? root.get() : folderGateway.create(Folder.createRoot(owner)); - return GetRootFolderOutput.from(folder, fileGateway.findByFolder(folder.getId())); + + return GetRootFolderOutput.from( + folder, + this.folderGateway.findByParentFolderId(folder.getId()), + fileGateway.findByFolder(folder.getId())); } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java index b29c31f3..f6aef742 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.List; +import java.util.Set; import java.util.UUID; import com.callv2.drive.domain.folder.Folder; @@ -12,21 +13,24 @@ public record GetRootFolderOutput( List files, Instant createdAt) { - public static GetRootFolderOutput from(final Folder folder, List files) { + public static GetRootFolderOutput from( + final Folder folder, + final Set subFolders, + final List files) { final var filesOutput = files != null ? files.stream().map(GetRootFolderOutput.File::from).toList() : null; return new GetRootFolderOutput( folder.getId().getValue(), - folder.getSubFolders().stream().map(GetRootFolderOutput.SubFolder::from).toList(), + subFolders.stream().map(GetRootFolderOutput.SubFolder::from).toList(), filesOutput, folder.getCreatedAt()); } public static record SubFolder(UUID id, String name) { - public static GetRootFolderOutput.SubFolder from(com.callv2.drive.domain.folder.SubFolder subFolder) { - return new SubFolder(subFolder.id().getValue(), subFolder.name().value()); + public static GetRootFolderOutput.SubFolder from(final Folder subFolder) { + return new SubFolder(subFolder.getId().getValue(), subFolder.getName().value()); } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/list/FolderListOutput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/list/FolderListOutput.java index f2c091fc..44bc5b62 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/list/FolderListOutput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/list/FolderListOutput.java @@ -1,6 +1,5 @@ package com.callv2.drive.application.folder.retrieve.list; -import java.util.List; import java.util.UUID; import com.callv2.drive.domain.folder.Folder; @@ -8,21 +7,19 @@ public record FolderListOutput( UUID id, String name, - UUID parentFolder, - List subFolders) { + UUID parentFolder) { - public static FolderListOutput from(Folder folder) { + public static FolderListOutput from(final Folder folder) { return new FolderListOutput( folder.getId().getValue(), folder.getName().value(), - folder.getParentFolder().getValue(), - folder.getSubFolders().stream().map(SubFolder::from).toList()); + folder.getParentFolder().getValue()); } public static record SubFolder(UUID id, String name) { - public static FolderListOutput.SubFolder from(com.callv2.drive.domain.folder.SubFolder subFolder) { - return new FolderListOutput.SubFolder(subFolder.id().getValue(), subFolder.name().value()); + public static FolderListOutput.SubFolder from(final Folder subFolder) { + return new FolderListOutput.SubFolder(subFolder.getId().getValue(), subFolder.getName().value()); } } diff --git a/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java index 963d79b5..1046607b 100644 --- a/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java @@ -49,7 +49,7 @@ void givenAValidFolderId_whenCallsExecute_thenShouldReturnFolder() { Folder.createRoot(ownerId)); final var expectedFolderId = expectedFolder.getId(); - final var expectedSubFolders = expectedFolder.getSubFolders(); + // final var expectedSubFolders = expectedFolder.getSubFolders(); final var expectedCreatedAt = expectedFolder.getCreatedAt(); final var expectedUpdatedAt = expectedFolder.getUpdatedAt(); final var expectedDeletedAt = expectedFolder.getDeletedAt(); @@ -64,7 +64,7 @@ void givenAValidFolderId_whenCallsExecute_thenShouldReturnFolder() { assertEquals(expectedFolderId.getValue(), actualOutput.id()); assertEquals(expectedFolderName, actualOutput.name()); assertEquals(expectedFolder.getParentFolder().getValue(), actualOutput.parentFolder()); - assertEquals(expectedSubFolders.size(), actualOutput.subFolders().size()); + // assertEquals(expectedSubFolders.size(), actualOutput.subFolders().size()); assertEquals(expectedCreatedAt, actualOutput.createdAt()); assertEquals(expectedUpdatedAt, actualOutput.updatedAt()); assertEquals(expectedDeletedAt, actualOutput.deletedAt()); diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java b/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java index 5dfa0ed5..35beb80d 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java @@ -1,8 +1,6 @@ package com.callv2.drive.domain.folder; import java.time.Instant; -import java.util.HashSet; -import java.util.Set; import com.callv2.drive.domain.AggregateRoot; import com.callv2.drive.domain.exception.ValidationException; @@ -19,7 +17,6 @@ public class Folder extends AggregateRoot { private FolderName name; private FolderID parentFolder; - private Set subFolders; private Instant createdAt; private Instant updatedAt; @@ -30,7 +27,6 @@ private Folder( final MemberID owner, final FolderName name, final FolderID parentFolder, - final Set subFolders, final Instant createdAt, final Instant updatedAt, final Instant deletedAt, @@ -40,7 +36,6 @@ private Folder( this.owner = owner; this.name = name; this.parentFolder = parentFolder; - this.subFolders = subFolders == null ? new HashSet<>() : new HashSet<>(subFolders); this.createdAt = createdAt; this.updatedAt = updatedAt; this.deletedAt = deletedAt; @@ -54,12 +49,11 @@ public static Folder with( final MemberID owner, final FolderName name, final FolderID parentFolder, - final Set subFolders, final Instant createdAt, final Instant updatedAt, final Instant deletedAt, final boolean rootFolder) { - return new Folder(id, owner, name, parentFolder, subFolders, createdAt, updatedAt, deletedAt, rootFolder); + return new Folder(id, owner, name, parentFolder, createdAt, updatedAt, deletedAt, rootFolder); } public static Folder createRoot(final MemberID owner) { @@ -70,7 +64,6 @@ public static Folder createRoot(final MemberID owner) { owner, FolderName.of("Root"), null, - new HashSet<>(), now, now, null, @@ -89,13 +82,11 @@ public static Folder create( owner, name, parentFolder.getId(), - new HashSet<>(), now, now, null, false); - parentFolder.addSubFolder(folder); return folder; } @@ -121,9 +112,6 @@ public Folder changeParentFolder(final Folder parentFolder) { final Notification notification = Notification.create(); - if (this.subFolders.stream().anyMatch(it -> it.id().equals(parentFolder.getId()))) - notification.append(ValidationError.with("Parent folder cannot be a subfolder")); - if (this.getId().equals(parentFolder.getId())) notification.append(ValidationError.with("Parent folder cannot be the same folder")); @@ -135,48 +123,6 @@ public Folder changeParentFolder(final Folder parentFolder) { return this; } - public Folder addSubFolder(final Folder folder) { - - if (folder == null) - return this; - - final SubFolder subFolder = SubFolder.from(folder); - - final Notification notification = Notification.create(); - - if (this.subFolders.contains(subFolder)) - notification - .append(ValidationError.with("SubFolder already exists")); - - if (this.subFolders.stream().anyMatch(it -> it.name().equals(folder.getName()))) - notification - .append(ValidationError.with("SubFolder %s already exists".formatted(folder.getName().value()))); - - if (notification.hasError()) - throw ValidationException.with("Error on add subfolder", notification); - - this.subFolders.add(subFolder); - this.updatedAt = Instant.now(); - - return this; - } - - public Folder removeSubFolder(final Folder folder) { - - if (folder == null) - return this; - - final SubFolder subFolder = SubFolder.from(folder); - - if (!this.subFolders.contains(subFolder)) - return this; - - this.subFolders.remove(subFolder); - this.updatedAt = Instant.now(); - - return this; - } - public boolean isRootFolder() { return rootFolder; } @@ -193,10 +139,6 @@ public FolderName getName() { return name; } - public Set getSubFolders() { - return Set.copyOf(subFolders); - } - public Instant getCreatedAt() { return createdAt; } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/FolderGateway.java b/domain/src/main/java/com/callv2/drive/domain/folder/FolderGateway.java index 6101dff5..b1fd7ce1 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/FolderGateway.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/FolderGateway.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import com.callv2.drive.domain.pagination.Page; import com.callv2.drive.domain.pagination.SearchQuery; @@ -10,6 +11,8 @@ public interface FolderGateway { Optional findRoot(); + Set findByParentFolderId(FolderID parentFolderId); + Folder create(Folder folder); Folder update(Folder folder); diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/SubFolder.java b/domain/src/main/java/com/callv2/drive/domain/folder/SubFolder.java deleted file mode 100644 index 0fa935fc..00000000 --- a/domain/src/main/java/com/callv2/drive/domain/folder/SubFolder.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.callv2.drive.domain.folder; - -import com.callv2.drive.domain.ValueObject; - -public record SubFolder(FolderID id, FolderName name) implements ValueObject { - - public static SubFolder with(final FolderID id, final FolderName name) { - return new SubFolder(id, name); - } - - public static SubFolder from(final Folder folder) { - return new SubFolder(folder.getId(), folder.getName()); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java index 281a9ce0..59365ad8 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java @@ -2,6 +2,8 @@ import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; @@ -35,6 +37,15 @@ public Optional findRoot() { return this.folderRepository.findByRootFolderTrue().map(FolderJpaEntity::toDomain); } + @Override + public Set findByParentFolderId(FolderID parentFolderId) { + return this.folderRepository + .findAllByParentFolderId(parentFolderId.getValue()) + .stream() + .map(FolderJpaEntity::toDomain) + .collect(Collectors.toSet()); + } + @Override public Folder create(Folder folder) { return save(folder); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/FolderListResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/FolderListResponse.java index 106da8be..a1dbe714 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/FolderListResponse.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/FolderListResponse.java @@ -1,16 +1,10 @@ package com.callv2.drive.infrastructure.folder.model; -import java.util.List; import java.util.UUID; public record FolderListResponse( UUID id, String name, - UUID parentFolder, - List subFolders) { - - public static record SubFolder(UUID id, String name) { - - } + UUID parentFolder) { } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaEntity.java index 53eb4d6d..87117222 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaEntity.java @@ -1,22 +1,16 @@ package com.callv2.drive.infrastructure.folder.persistence; import java.time.Instant; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; import com.callv2.drive.domain.folder.Folder; import com.callv2.drive.domain.folder.FolderID; import com.callv2.drive.domain.folder.FolderName; -import com.callv2.drive.domain.folder.SubFolder; import com.callv2.drive.domain.member.MemberID; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity(name = "Folder") @@ -47,9 +41,6 @@ public class FolderJpaEntity { @Column(name = "deleted_at") private Instant deletedAt; - @OneToMany(mappedBy = "parentFolder", cascade = CascadeType.ALL, orphanRemoval = true) - private Set subFolders; - private FolderJpaEntity( final UUID id, final Boolean rootFolder, @@ -68,7 +59,6 @@ private FolderJpaEntity( this.updatedAt = updatedAt; this.deletedAt = deletedAt; - this.subFolders = new HashSet<>(); } public FolderJpaEntity() { @@ -87,9 +77,6 @@ public static FolderJpaEntity fromDomain(final Folder folder) { folder.getUpdatedAt(), folder.getDeletedAt()); - folder.getSubFolders() - .forEach(entity::addSubFolder); - return entity; } @@ -99,19 +86,12 @@ public Folder toDomain() { MemberID.of(ownerId), FolderName.of(name), FolderID.of(parentFolderId), - subFolders.stream() - .map(SubFolderJpaEntity::toDomain) - .collect(Collectors.toSet()), createdAt, updatedAt, deletedAt, rootFolder); } - public void addSubFolder(final SubFolder anId) { - this.subFolders.add(SubFolderJpaEntity.with(this, anId)); - } - public UUID getId() { return id; } @@ -152,14 +132,6 @@ public void setParentFolderId(UUID parentFolderId) { this.parentFolderId = parentFolderId; } - public Set getSubFolders() { - return subFolders; - } - - public void setSubFolders(Set subFolders) { - this.subFolders = subFolders; - } - public Instant getCreatedAt() { return createdAt; } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaRepository.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaRepository.java index 75de0db4..a3594240 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaRepository.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaRepository.java @@ -1,5 +1,6 @@ package com.callv2.drive.infrastructure.folder.persistence; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -12,6 +13,8 @@ public interface FolderJpaRepository extends JpaRepository findByRootFolderTrue(); + List findAllByParentFolderId(UUID parentFolderId); + Page findAll(Specification whereClause, Pageable page); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderID.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderID.java deleted file mode 100644 index 44567fc7..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderID.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.callv2.drive.infrastructure.folder.persistence; - -import java.io.Serializable; -import java.util.UUID; - -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; - -@Embeddable -public class SubFolderID implements Serializable { - - @Column(name = "parent_folder_id", nullable = false) - private UUID parentFolderId; - - @Column(name = "sub_folder_id", nullable = false) - private UUID subFolderId; - - public SubFolderID() { - } - - private SubFolderID(UUID parentFolderId, UUID subFolderId) { - this.parentFolderId = parentFolderId; - this.subFolderId = subFolderId; - } - - public static SubFolderID with(UUID parentFolderId, UUID subFolderId) { - return new SubFolderID(parentFolderId, subFolderId); - } - - public UUID getParentFolderId() { - return parentFolderId; - } - - public void setParentFolderId(UUID parentFolderId) { - this.parentFolderId = parentFolderId; - } - - public UUID getSubFolderId() { - return subFolderId; - } - - public void setSubFolderId(UUID subFolderId) { - this.subFolderId = subFolderId; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((parentFolderId == null) ? 0 : parentFolderId.hashCode()); - result = prime * result + ((subFolderId == null) ? 0 : subFolderId.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SubFolderID other = (SubFolderID) obj; - if (parentFolderId == null) { - if (other.parentFolderId != null) - return false; - } else if (!parentFolderId.equals(other.parentFolderId)) - return false; - if (subFolderId == null) { - if (other.subFolderId != null) - return false; - } else if (!subFolderId.equals(other.subFolderId)) - return false; - return true; - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderJpaEntity.java deleted file mode 100644 index 1887135a..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderJpaEntity.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.callv2.drive.infrastructure.folder.persistence; - -import com.callv2.drive.domain.folder.FolderID; -import com.callv2.drive.domain.folder.FolderName; -import com.callv2.drive.domain.folder.SubFolder; - -import jakarta.persistence.Column; -import jakarta.persistence.EmbeddedId; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.MapsId; -import jakarta.persistence.Table; - -@Entity(name = "SubFolder") -@Table(name = "sub_folders") -public class SubFolderJpaEntity { - - @EmbeddedId - private SubFolderID id; - - @Column(name = "name", nullable = false) - private String name; - - @ManyToOne(fetch = FetchType.LAZY) - @MapsId("parentFolderId") - private FolderJpaEntity parentFolder; - - public SubFolderJpaEntity() { - } - - private SubFolderJpaEntity( - final SubFolderID id, - final String name, - final FolderJpaEntity parentFolder) { - this.id = id; - this.name = name; - this.parentFolder = parentFolder; - } - - public static SubFolderJpaEntity with(final FolderJpaEntity folder, final SubFolder subFolder) { - return new SubFolderJpaEntity( - SubFolderID.with(folder.getId(), subFolder.id().getValue()), - subFolder.name().value(), - folder); - } - - public SubFolder toDomain() { - return SubFolder.with(FolderID.of(getId().getSubFolderId()), FolderName.of(getName())); - } - - public SubFolderID getId() { - return id; - } - - public void setId(SubFolderID id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public FolderJpaEntity getParentFolder() { - return parentFolder; - } - - public void setParentFolder(FolderJpaEntity parentFolder) { - this.parentFolder = parentFolder; - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java index 479251f0..8d6296d6 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java @@ -72,12 +72,7 @@ static FolderListResponse present(FolderListOutput output) { return new FolderListResponse( output.id(), output.name(), - output.parentFolder(), - output.subFolders().stream().map(FolderPresenter::present).toList()); - } - - static FolderListResponse.SubFolder present(FolderListOutput.SubFolder subFolder) { - return new FolderListResponse.SubFolder(subFolder.id(), subFolder.name()); + output.parentFolder()); } } From 49d01f1b60186aa2fb82db8643854b7ad99475d2 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 13 Jun 2025 18:46:57 -0300 Subject: [PATCH 60/89] test: enhance folder use case tests with additional verifications --- .../move/DefaultMoveFolderUseCaseTest.java | 15 ++- .../get/DefaultGetFolderUseCaseTest.java | 116 ++++++++++-------- 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java index e079f46e..68f9d6a2 100644 --- a/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java @@ -1,12 +1,15 @@ package com.callv2.drive.application.folder.move; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,9 +50,8 @@ void givenVAlidInput_whenCallsExecute_thenMoveFolder() { when(folderGateway.findById(expectedRootFolder.getId())) .thenReturn(Optional.of(expectedRootFolder)); - doNothing() - .when(folderGateway) - .updateAll(anyList()); + when(folderGateway.findByParentFolderId(expectedFolderTarget.getId())) + .thenReturn(Set.of()); final var input = new MoveFolderInput( expectedFolderToMove.getId().getValue(), @@ -57,7 +59,10 @@ void givenVAlidInput_whenCallsExecute_thenMoveFolder() { assertDoesNotThrow(() -> useCase.execute(input)); - verify(folderGateway).updateAll(anyList()); + verify(folderGateway, never()).updateAll(anyList()); + + verify(folderGateway, times(1)).findByParentFolderId(any()); + } } diff --git a/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java index 1046607b..2a89f6b2 100644 --- a/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.when; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,75 +29,92 @@ @ExtendWith(MockitoExtension.class) public class DefaultGetFolderUseCaseTest { - @InjectMocks - DefaultGetFolderUseCase useCase; + @InjectMocks + DefaultGetFolderUseCase useCase; - @Mock - FolderGateway folderGateway; + @Mock + FolderGateway folderGateway; - @Mock - FileGateway fileGateway; + @Mock + FileGateway fileGateway; - @Test - void givenAValidFolderId_whenCallsExecute_thenShouldReturnFolder() { + @Test + void givenAValidFolderId_whenCallsExecute_thenShouldReturnFolder() { - final var ownerId = MemberID.of("owner"); + final var ownerId = MemberID.of("owner"); - final var expectedFolderName = "folder"; - final var expectedFolder = Folder.create( - ownerId, - FolderName.of(expectedFolderName), - Folder.createRoot(ownerId)); + final var expectedFolderName = "folder"; + final var expectedFolder = Folder.create( + ownerId, + FolderName.of(expectedFolderName), + Folder.createRoot(ownerId)); - final var expectedFolderId = expectedFolder.getId(); - // final var expectedSubFolders = expectedFolder.getSubFolders(); - final var expectedCreatedAt = expectedFolder.getCreatedAt(); - final var expectedUpdatedAt = expectedFolder.getUpdatedAt(); - final var expectedDeletedAt = expectedFolder.getDeletedAt(); + final var expectedSubFolder1 = Folder.create( + ownerId, + FolderName.of("subFolder1"), + expectedFolder); + final var expectedSubFolder2 = Folder.create( + ownerId, + FolderName.of("subFolder2"), + expectedFolder); - when(folderGateway.findById(expectedFolderId)) - .thenReturn(Optional.of(expectedFolder)); + final var expectedSubFolders = Set.of(expectedSubFolder1, expectedSubFolder2); - final var input = GetFolderInput.with(expectedFolderId.getValue()); + final var expectedFolderId = expectedFolder.getId(); + final var expectedCreatedAt = expectedFolder.getCreatedAt(); + final var expectedUpdatedAt = expectedFolder.getUpdatedAt(); + final var expectedDeletedAt = expectedFolder.getDeletedAt(); - final var actualOutput = assertDoesNotThrow(() -> useCase.execute(input)); + when(folderGateway.findById(expectedFolderId)) + .thenReturn(Optional.of(expectedFolder)); - assertEquals(expectedFolderId.getValue(), actualOutput.id()); - assertEquals(expectedFolderName, actualOutput.name()); - assertEquals(expectedFolder.getParentFolder().getValue(), actualOutput.parentFolder()); - // assertEquals(expectedSubFolders.size(), actualOutput.subFolders().size()); - assertEquals(expectedCreatedAt, actualOutput.createdAt()); - assertEquals(expectedUpdatedAt, actualOutput.updatedAt()); - assertEquals(expectedDeletedAt, actualOutput.deletedAt()); + when(folderGateway.findByParentFolderId(expectedFolder.getId())) + .thenReturn(expectedSubFolders); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + final var input = GetFolderInput.with(expectedFolderId.getValue()); - } + final var actualOutput = assertDoesNotThrow(() -> useCase.execute(input)); - @Test - void givenNotExistentFolderId_whenCallsExecute_thenShouldThorwsNotFoundException() { + assertEquals(expectedFolderId.getValue(), actualOutput.id()); + assertEquals(expectedFolderName, actualOutput.name()); + assertEquals(expectedFolder.getParentFolder().getValue(), actualOutput.parentFolder()); + assertEquals(expectedSubFolders.size(), actualOutput.subFolders().size()); + assertEquals(expectedCreatedAt, actualOutput.createdAt()); + assertEquals(expectedUpdatedAt, actualOutput.updatedAt()); + assertEquals(expectedDeletedAt, actualOutput.deletedAt()); - final var expectedFolderId = FolderID.unique(); + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); - final var expectedExceptionMessage = "[Folder] not found."; - final var expectedErrorCount = 1; - final var expectedErrorMessage = "[Folder] with id [%s] not found.".formatted(expectedFolderId.getValue()); + verify(folderGateway, times(1)).findByParentFolderId(any()); + verify(folderGateway, times(1)).findByParentFolderId(eq(expectedFolder.getId())); - when(folderGateway.findById(expectedFolderId)) - .thenReturn(Optional.empty()); + } - final var input = GetFolderInput.with(expectedFolderId.getValue()); + @Test + void givenNotExistentFolderId_whenCallsExecute_thenShouldThorwsNotFoundException() { - final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + final var expectedFolderId = FolderID.unique(); - assertEquals(expectedExceptionMessage, actualException.getMessage()); - assertEquals(expectedErrorCount, actualException.getErrors().size()); - assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + final var expectedExceptionMessage = "[Folder] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Folder] with id [%s] not found." + .formatted(expectedFolderId.getValue()); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + when(folderGateway.findById(expectedFolderId)) + .thenReturn(Optional.empty()); - } + final var input = GetFolderInput.with(expectedFolderId.getValue()); + + final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + + } } From 08dd131dc7a1001f98e53d38ab36a0f4ac6aa0a7 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Fri, 13 Jun 2025 21:11:19 -0300 Subject: [PATCH 61/89] feat: update folder response models to include ownerId and isRootFolder, and refactor related API methods --- .../folder/retrieve/get/GetFolderOutput.java | 16 +++-- .../get/root/GetRootFolderOutput.java | 10 ++- .../drive/infrastructure/api/FolderAPI.java | 3 +- .../api/controller/FolderController.java | 3 +- .../folder/model/GetFolderResponse.java | 8 ++- .../folder/model/GetRootFolderResponse.java | 24 ------- .../folder/presenter/FolderPresenter.java | 65 ++++++++++++------- 7 files changed, 70 insertions(+), 59 deletions(-) delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetRootFolderResponse.java diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java index 71b1b772..9c0a9c55 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java @@ -4,15 +4,18 @@ import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import com.callv2.drive.domain.folder.Folder; public record GetFolderOutput( UUID id, String name, + Boolean isRootFolder, UUID parentFolder, - List subFolders, - List files, + Set subFolders, + Set files, + String ownerId, Instant createdAt, Instant updatedAt, Instant deletedAt) { @@ -25,12 +28,17 @@ public static GetFolderOutput from( return new GetFolderOutput( folder.getId().getValue(), folder.getName().value(), + folder.isRootFolder(), folder.getParentFolder().getValue(), subFolders .stream() .map(GetFolderOutput.SubFolder::from) - .toList(), - files.stream().map(GetFolderOutput.File::from).toList(), + .collect(Collectors.toSet()), + files + .stream() + .map(GetFolderOutput.File::from) + .collect(Collectors.toSet()), + folder.getOwner().getValue(), folder.getCreatedAt(), folder.getUpdatedAt(), folder.getDeletedAt()); diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java index f6aef742..f34fb9f7 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java @@ -9,9 +9,12 @@ public record GetRootFolderOutput( UUID id, + String name, List subFolders, List files, - Instant createdAt) { + String ownerId, + Instant createdAt, + Instant updatedAt) { public static GetRootFolderOutput from( final Folder folder, @@ -22,9 +25,12 @@ public static GetRootFolderOutput from( return new GetRootFolderOutput( folder.getId().getValue(), + folder.getName().value(), subFolders.stream().map(GetRootFolderOutput.SubFolder::from).toList(), filesOutput, - folder.getCreatedAt()); + folder.getOwner().getValue(), + folder.getCreatedAt(), + folder.getUpdatedAt()); } public static record SubFolder(UUID id, String name) { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java index 62856f1b..04b38198 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java @@ -21,7 +21,6 @@ import com.callv2.drive.infrastructure.folder.model.CreateFolderResponse; import com.callv2.drive.infrastructure.folder.model.FolderListResponse; import com.callv2.drive.infrastructure.folder.model.GetFolderResponse; -import com.callv2.drive.infrastructure.folder.model.GetRootFolderResponse; import com.callv2.drive.infrastructure.folder.model.MoveFolderRequest; import io.swagger.v3.oas.annotations.Operation; @@ -42,7 +41,7 @@ public interface FolderAPI { @ApiResponse(responseCode = "200", description = "Root folder retrived successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) }) - ResponseEntity getRoot(); + ResponseEntity getRoot(); @PostMapping(consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE }) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java index 1a13f32e..e8d83a9f 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java @@ -25,7 +25,6 @@ import com.callv2.drive.infrastructure.folder.model.CreateFolderResponse; import com.callv2.drive.infrastructure.folder.model.FolderListResponse; import com.callv2.drive.infrastructure.folder.model.GetFolderResponse; -import com.callv2.drive.infrastructure.folder.model.GetRootFolderResponse; import com.callv2.drive.infrastructure.folder.model.MoveFolderRequest; import com.callv2.drive.infrastructure.folder.presenter.FolderPresenter; import com.callv2.drive.infrastructure.security.SecurityContext; @@ -53,7 +52,7 @@ public FolderController( } @Override - public ResponseEntity getRoot() { + public ResponseEntity getRoot() { return ResponseEntity.ok(FolderPresenter.present( getRootFolderUseCase.execute(GetRootFolderInput.from(SecurityContext.getAuthenticatedUser())))); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetFolderResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetFolderResponse.java index 3b5c9274..9f598e37 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetFolderResponse.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetFolderResponse.java @@ -1,15 +1,17 @@ package com.callv2.drive.infrastructure.folder.model; import java.time.Instant; -import java.util.List; +import java.util.Set; import java.util.UUID; public record GetFolderResponse( UUID id, String name, + Boolean rootFolder, UUID parentFolder, - List subFolders, - List files, + Set subFolders, + Set files, + String ownerId, Instant createdAt, Instant updatedAt) { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetRootFolderResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetRootFolderResponse.java deleted file mode 100644 index 017ab681..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetRootFolderResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.callv2.drive.infrastructure.folder.model; - -import java.time.Instant; -import java.util.List; -import java.util.UUID; - -public record GetRootFolderResponse( - UUID id, - List subFolders, - List files, - Instant createdAt) { - - public static record SubFolder(UUID id, String name) { - } - - public static record File( - UUID id, - String name, - Long size, - Instant createdAt, - Instant updatedAt) { - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java index 8d6296d6..257ae6f3 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java @@ -1,5 +1,7 @@ package com.callv2.drive.infrastructure.folder.presenter; +import java.util.stream.Collectors; + import com.callv2.drive.application.folder.create.CreateFolderOutput; import com.callv2.drive.application.folder.retrieve.get.GetFolderOutput; import com.callv2.drive.application.folder.retrieve.get.root.GetRootFolderOutput; @@ -7,60 +9,70 @@ import com.callv2.drive.infrastructure.folder.model.CreateFolderResponse; import com.callv2.drive.infrastructure.folder.model.FolderListResponse; import com.callv2.drive.infrastructure.folder.model.GetFolderResponse; -import com.callv2.drive.infrastructure.folder.model.GetRootFolderResponse; public interface FolderPresenter { - static CreateFolderResponse present(CreateFolderOutput output) { + static CreateFolderResponse present(final CreateFolderOutput output) { return new CreateFolderResponse(output.id()); } - static GetFolderResponse present(GetFolderOutput output) { + static GetFolderResponse present(final GetFolderOutput output) { return new GetFolderResponse( output.id(), output.name(), + output.isRootFolder(), output.parentFolder(), output.subFolders().stream() .map(FolderPresenter::present) - .toList(), + .collect(Collectors.toSet()), output.files() .stream() .map(FolderPresenter::present) - .toList(), + .collect(Collectors.toSet()), + output.ownerId(), output.createdAt(), output.updatedAt()); } - static GetFolderResponse.SubFolder present(GetFolderOutput.SubFolder subFolder) { + static GetFolderResponse.SubFolder present(final GetFolderOutput.SubFolder subFolder) { return new GetFolderResponse.SubFolder(subFolder.id(), subFolder.name()); } - static GetFolderResponse.File present(GetFolderOutput.File subFolder) { + static GetFolderResponse.File present(final GetFolderOutput.File file) { return new GetFolderResponse.File( - subFolder.id(), - subFolder.name(), - subFolder.size(), - subFolder.createdAt(), - subFolder.updatedAt()); + file.id(), + file.name(), + file.size(), + file.createdAt(), + file.updatedAt()); } - static GetRootFolderResponse present(GetRootFolderOutput output) { - return new GetRootFolderResponse( + static GetFolderResponse present(final GetRootFolderOutput output) { + return new GetFolderResponse( output.id(), - output.subFolders().stream().map(FolderPresenter::present).toList(), + output.name(), + true, + null, + output + .subFolders() + .stream() + .map(FolderPresenter::present) + .collect(Collectors.toSet()), output.files() .stream() .map(FolderPresenter::present) - .toList(), - output.createdAt()); + .collect(Collectors.toSet()), + output.ownerId(), + output.createdAt(), + output.updatedAt()); } - static GetRootFolderResponse.SubFolder present(GetRootFolderOutput.SubFolder subFolder) { - return new GetRootFolderResponse.SubFolder(subFolder.id(), subFolder.name()); + static GetFolderResponse.SubFolder present(final GetRootFolderOutput.SubFolder subFolder) { + return new GetFolderResponse.SubFolder(subFolder.id(), subFolder.name()); } - static GetRootFolderResponse.File present(GetRootFolderOutput.File subFolder) { - return new GetRootFolderResponse.File( + static GetFolderResponse.File present(final GetRootFolderOutput.File subFolder) { + return new GetFolderResponse.File( subFolder.id(), subFolder.name(), subFolder.size(), @@ -68,7 +80,16 @@ static GetRootFolderResponse.File present(GetRootFolderOutput.File subFolder) { subFolder.updatedAt()); } - static FolderListResponse present(FolderListOutput output) { + static GetFolderResponse.File present(final GetFolderResponse.File file) { + return new GetFolderResponse.File( + file.id(), + file.name(), + file.size(), + file.createdAt(), + file.updatedAt()); + } + + static FolderListResponse present(final FolderListOutput output) { return new FolderListResponse( output.id(), output.name(), From 97717fcf36f8a745d90e45f261461d64f1a2d5e6 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 14 Jun 2025 18:48:51 -0300 Subject: [PATCH 62/89] fix: correct spelling of 'validate' in multiple locations across the codebase --- .../drive/application/file/create/DefaultCreateFileUseCase.java | 2 +- .../application/folder/create/DefaultCreateFolderUseCase.java | 2 +- .../quota/request/create/DefaultCreateRequestQuotaUseCase.java | 2 +- .../com/callv2/drive/domain/validation/ValidationHandler.java | 2 +- .../callv2/drive/domain/validation/handler/Notification.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java index 7cc89042..26fc101a 100644 --- a/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java @@ -84,7 +84,7 @@ public CreateFileOutput execute(final CreateFileInput input) { final Content content = Content.of(contentLocation, contentType, contentSize); - final File file = notification.valdiate(() -> File.create(ownerId, folderId, fileName, content)); + final File file = notification.validate(() -> File.create(ownerId, folderId, fileName, content)); if (notification.hasError()) throw ValidationException.with("Could not create Aggregate File", notification); diff --git a/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java index 70522841..72c8cb2d 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java @@ -49,7 +49,7 @@ private Folder createFolder(final MemberID ownerId, FolderName name, final Folde if (subFolders.stream().anyMatch(subFolder -> subFolder.getName().equals(name))) notification.append(ValidationError.with("Folder with the same name already exists")); - final Folder folder = notification.valdiate(() -> Folder.create(ownerId, name, parentFolder)); + final Folder folder = notification.validate(() -> Folder.create(ownerId, name, parentFolder)); if (notification.hasError()) throw ValidationException.with("Could not create Aggregate Folder", notification); diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java index dcff72ab..53f56acb 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java @@ -28,7 +28,7 @@ public void execute(final CreateRequestQuotaInput input) { .orElseThrow(() -> NotFoundException.with(Member.class, input.memberId())); final Notification notification = Notification.create(); - notification.valdiate(() -> member.requestQuota(Quota.of(input.ammount(), input.unit()))); + notification.validate(() -> member.requestQuota(Quota.of(input.ammount(), input.unit()))); if (notification.hasError()) throw ValidationException.with("Request Quota Error", notification); diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java index c4d1fc50..27a7965a 100644 --- a/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java +++ b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java @@ -8,7 +8,7 @@ public interface ValidationHandler { ValidationHandler append(final ValidationHandler handler); - T valdiate(final Validation validation); + T validate(final Validation validation); List getErrors(); diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java b/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java index 4388d2a8..14cca621 100644 --- a/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java +++ b/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java @@ -36,7 +36,7 @@ public Notification append(final ValidationHandler handler) { } @Override - public T valdiate(final Validation validation) { + public T validate(final Validation validation) { try { return validation.validate(); } catch (DomainException e) { From e8e281b66c965fffc428b9cc7eddb60eb0fb5770 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 14 Jun 2025 18:50:30 -0300 Subject: [PATCH 63/89] fix: correct spelling of 'retrive' to 'retrieve' in API response descriptions --- .../java/com/callv2/drive/infrastructure/api/FileAPI.java | 2 +- .../java/com/callv2/drive/infrastructure/api/FolderAPI.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java index 91ff25d7..6b8799fa 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java @@ -60,7 +60,7 @@ ResponseEntity create( @GetMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a file", description = "This method retrive a file", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses({ - @ApiResponse(responseCode = "200", description = "File retrived successfully", content = @Content(schema = @Schema(implementation = GetFileResponse.class))), + @ApiResponse(responseCode = "200", description = "File retrieved successfully", content = @Content(schema = @Schema(implementation = GetFileResponse.class))), @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))), @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) }) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java index 04b38198..a3031648 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java @@ -38,7 +38,7 @@ public interface FolderAPI { @GetMapping(value = "root", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a folder", description = "This method retrive a root folder", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Root folder retrived successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), + @ApiResponse(responseCode = "200", description = "Root folder retrieved successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) }) ResponseEntity getRoot(); @@ -56,7 +56,7 @@ public interface FolderAPI { @GetMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a folder", description = "This method retrive a folder", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Folder retrived successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), + @ApiResponse(responseCode = "200", description = "Folder retrieved successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) }) From 4d5bf040539bd69c69cca20fc415656a1607a695 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 14 Jun 2025 18:52:18 -0300 Subject: [PATCH 64/89] test: fix verification of folder update in DefaultMoveFolderUseCaseTest --- .../application/folder/move/DefaultMoveFolderUseCaseTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java index 68f9d6a2..574eaf7a 100644 --- a/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -60,7 +61,7 @@ void givenVAlidInput_whenCallsExecute_thenMoveFolder() { assertDoesNotThrow(() -> useCase.execute(input)); verify(folderGateway, never()).updateAll(anyList()); - + verify(folderGateway, times(1)).update(eq(expectedFolderToMove)); verify(folderGateway, times(1)).findByParentFolderId(any()); } From cfdf1e9c5983525be5381829c308fba5865be49f Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 3 Aug 2025 20:40:09 -0300 Subject: [PATCH 65/89] feat: add AOP module with context and executor classes for handling pre and post invocation processes --- aop/build.gradle | 30 +++++++ .../context/AbstractInvocationContext.java | 66 ++++++++++++++ .../aop/context/PostInvocationContext.java | 17 ++++ .../aop/context/PreInvocationContext.java | 15 ++++ .../context/SimplePostInvocationContext.java | 86 +++++++++++++++++++ .../context/SimplePreInvocationContext.java | 52 +++++++++++ .../com/callv2/aop/executor/Executor.java | 9 ++ .../com/callv2/aop/executor/PostExecutor.java | 7 ++ .../com/callv2/aop/executor/PreExecutor.java | 7 ++ .../aop/executor/chain/ExecutorChain.java | 32 +++++++ .../chain/PostInvocationExecutorChain.java | 21 +++++ .../chain/PreInvocationExecutorChain.java | 19 ++++ .../chain/handler/ExecutorChainHandler.java | 10 +++ .../handler/SimpleExecutorChainHandler.java | 44 ++++++++++ .../SimpleExecutorChainHandlerBuilder.java | 73 ++++++++++++++++ settings.gradle | 2 +- 16 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 aop/build.gradle create mode 100644 aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java create mode 100644 aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java create mode 100644 aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java create mode 100644 aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java create mode 100644 aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/Executor.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/PostExecutor.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/PreExecutor.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/chain/ExecutorChain.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/chain/handler/ExecutorChainHandler.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java create mode 100644 aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java diff --git a/aop/build.gradle b/aop/build.gradle new file mode 100644 index 00000000..2bf2e09c --- /dev/null +++ b/aop/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java-library' +} + +group = 'com.callv2' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + + implementation 'org.aspectj:aspectjrt:1.9.24' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' + testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java new file mode 100644 index 00000000..e6f06f3e --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java @@ -0,0 +1,66 @@ +package com.callv2.aop.context; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.reflect.SourceLocation; +import org.aspectj.runtime.internal.AroundClosure; + +public abstract class AbstractInvocationContext implements ProceedingJoinPoint { + + protected final ProceedingJoinPoint joinPoint; + + protected AbstractInvocationContext(final ProceedingJoinPoint joinPoint) { + this.joinPoint = joinPoint; + } + + @Override + public void set$AroundClosure(AroundClosure arc) { + this.joinPoint.set$AroundClosure(arc); + } + + @Override + public String toShortString() { + return this.joinPoint.toShortString(); + } + + @Override + public String toLongString() { + return this.joinPoint.toLongString(); + } + + @Override + public Object getThis() { + return this.joinPoint.getThis(); + } + + @Override + public Object getTarget() { + return this.joinPoint.getTarget(); + } + + @Override + public Object[] getArgs() { + return this.joinPoint.getArgs(); + } + + @Override + public Signature getSignature() { + return this.joinPoint.getSignature(); + } + + @Override + public SourceLocation getSourceLocation() { + return this.joinPoint.getSourceLocation(); + } + + @Override + public String getKind() { + return this.joinPoint.getKind(); + } + + @Override + public StaticPart getStaticPart() { + return this.joinPoint.getStaticPart(); + } + +} diff --git a/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java new file mode 100644 index 00000000..74ef6ea8 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java @@ -0,0 +1,17 @@ +package com.callv2.aop.context; + +import java.time.Instant; + +import org.aspectj.lang.ProceedingJoinPoint; + +public interface PostInvocationContext extends ProceedingJoinPoint { + + Instant getProceededAt(); + + Object getResult(); + + Throwable getThrowable(); + + boolean wasSuccessful(); + +} \ No newline at end of file diff --git a/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java new file mode 100644 index 00000000..33387ab6 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java @@ -0,0 +1,15 @@ +package com.callv2.aop.context; + +import java.time.Instant; + +import org.aspectj.lang.ProceedingJoinPoint; + +public interface PreInvocationContext extends ProceedingJoinPoint { + + Instant getContextedAt(); + + boolean proceeded(); + + PostInvocationContext proceedWithContext(); + +} diff --git a/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java new file mode 100644 index 00000000..1ed55e3b --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java @@ -0,0 +1,86 @@ +package com.callv2.aop.context; + +import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.aspectj.lang.ProceedingJoinPoint; + +public final class SimplePostInvocationContext extends AbstractInvocationContext implements PostInvocationContext { + + private final Object result; + private final Throwable throwable; + private final Instant proceededAt; + private final AtomicBoolean successful; + + private SimplePostInvocationContext( + final ProceedingJoinPoint joinPoint, + final Object result, + final Throwable throwable, + final Instant proceededAt, + final boolean successful) { + super(joinPoint); + this.result = result; + this.throwable = throwable; + this.proceededAt = proceededAt; + this.successful = new AtomicBoolean(successful); + } + + protected static SimplePostInvocationContext from(final PreInvocationContext preInvocationContext) { + + Object result; + Throwable throwable; + boolean successful; + final Instant proceededAt; + + try { + result = preInvocationContext.proceed(); + throwable = null; + successful = true; + } catch (Throwable e) { + result = null; + throwable = e; + successful = false; + } finally { + proceededAt = Instant.now(); + } + + return new SimplePostInvocationContext( + preInvocationContext, + result, + throwable, + proceededAt, + successful); + + } + + @Override + public Object proceed() throws Throwable { + return this.joinPoint.proceed(); + } + + @Override + public Object proceed(Object[] args) throws Throwable { + return this.joinPoint.proceed(args); + } + + @Override + public Instant getProceededAt() { + return this.proceededAt; + } + + @Override + public Object getResult() { + return this.result; + } + + @Override + public Throwable getThrowable() { + return this.throwable; + } + + @Override + public boolean wasSuccessful() { + return this.successful.get(); + } + +} diff --git a/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java new file mode 100644 index 00000000..45adc925 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java @@ -0,0 +1,52 @@ +package com.callv2.aop.context; + +import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.aspectj.lang.ProceedingJoinPoint; + +public final class SimplePreInvocationContext extends AbstractInvocationContext implements PreInvocationContext { + + private final Instant contextedAt; + private final AtomicBoolean proceeded; + + private SimplePreInvocationContext(final ProceedingJoinPoint joinPoint) { + super(joinPoint); + this.contextedAt = Instant.now(); + this.proceeded = new AtomicBoolean(false); + } + + public static SimplePreInvocationContext of(final ProceedingJoinPoint joinPoint) { + return new SimplePreInvocationContext(joinPoint); + } + + @Override + public Object proceed() throws Throwable { + if (proceeded.getAndSet(true)) + throw new IllegalStateException("Method already proceeded"); + return joinPoint.proceed(); + } + + @Override + public Object proceed(Object[] args) throws Throwable { + if (proceeded.getAndSet(true)) + throw new IllegalStateException("Method already proceeded"); + return joinPoint.proceed(args); + } + + @Override + public Instant getContextedAt() { + return this.contextedAt; + } + + @Override + public boolean proceeded() { + return this.proceeded.get(); + } + + @Override + public PostInvocationContext proceedWithContext() { + return SimplePostInvocationContext.from(this); + } + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/Executor.java b/aop/src/main/java/com/callv2/aop/executor/Executor.java new file mode 100644 index 00000000..27458aef --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/Executor.java @@ -0,0 +1,9 @@ +package com.callv2.aop.executor; + +import org.aspectj.lang.ProceedingJoinPoint; + +public interface Executor { + + void execute(J joinPoint); + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/PostExecutor.java b/aop/src/main/java/com/callv2/aop/executor/PostExecutor.java new file mode 100644 index 00000000..37642b05 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/PostExecutor.java @@ -0,0 +1,7 @@ +package com.callv2.aop.executor; + +import com.callv2.aop.context.PostInvocationContext; + +public interface PostExecutor extends Executor { + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/PreExecutor.java b/aop/src/main/java/com/callv2/aop/executor/PreExecutor.java new file mode 100644 index 00000000..1fcf32b0 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/PreExecutor.java @@ -0,0 +1,7 @@ +package com.callv2.aop.executor; + +import com.callv2.aop.context.PreInvocationContext; + +public interface PreExecutor extends Executor { + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/ExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/ExecutorChain.java new file mode 100644 index 00000000..44397761 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/ExecutorChain.java @@ -0,0 +1,32 @@ +package com.callv2.aop.executor.chain; + +import org.aspectj.lang.ProceedingJoinPoint; + +import com.callv2.aop.executor.Executor; + +public abstract class ExecutorChain> { + + private ExecutorChain next; + private final E executor; + + protected ExecutorChain(final E executor) { + this.executor = executor; + } + + public ExecutorChain setNext(final ExecutorChain executorChain) { + return this.next = executorChain; + } + + public final O execute(final J joinpoint) throws Throwable { + + this.executor.execute(joinpoint); + + if (next != null) + return next.execute(joinpoint); + + return resolve(joinpoint); + } + + protected abstract O resolve(J joinPoint) throws Throwable; + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java new file mode 100644 index 00000000..d5ebe7d5 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java @@ -0,0 +1,21 @@ +package com.callv2.aop.executor.chain; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.executor.Executor; + +public final class PostInvocationExecutorChain + extends ExecutorChain> { + + public PostInvocationExecutorChain(final Executor executor) { + super(executor); + } + + @Override + protected Object resolve(final PostInvocationContext joinPoint) throws Throwable { + if (joinPoint.wasSuccessful()) + return joinPoint.getResult(); + else + throw joinPoint.getThrowable(); + } + +} \ No newline at end of file diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java new file mode 100644 index 00000000..e2509df3 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java @@ -0,0 +1,19 @@ +package com.callv2.aop.executor.chain; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.Executor; + +public class PreInvocationExecutorChain + extends ExecutorChain> { + + public PreInvocationExecutorChain(final Executor executor) { + super(executor); + } + + @Override + protected PostInvocationContext resolve(final PreInvocationContext joinPoint) throws Throwable { + return joinPoint.proceedWithContext(); + } + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/ExecutorChainHandler.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/ExecutorChainHandler.java new file mode 100644 index 00000000..3d214c6b --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/ExecutorChainHandler.java @@ -0,0 +1,10 @@ +package com.callv2.aop.executor.chain.handler; + +import org.aspectj.lang.ProceedingJoinPoint; + +@FunctionalInterface +public interface ExecutorChainHandler { + + Object handle(final ProceedingJoinPoint joinPoint) throws Throwable; + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java new file mode 100644 index 00000000..cc4b31d3 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java @@ -0,0 +1,44 @@ +package com.callv2.aop.executor.chain.handler; + +import org.aspectj.lang.ProceedingJoinPoint; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.context.SimplePreInvocationContext; +import com.callv2.aop.executor.chain.PostInvocationExecutorChain; +import com.callv2.aop.executor.chain.PreInvocationExecutorChain; + +public final class SimpleExecutorChainHandler implements ExecutorChainHandler { + + private final PreInvocationExecutorChain preInvocationExecutorChain; + private final PostInvocationExecutorChain postInvocationExecutorChain; + private final PostInvocationExecutorChain errorInvocationExecutorChain; + + public SimpleExecutorChainHandler( + final PreInvocationExecutorChain preInvocationExecutorChain, + final PostInvocationExecutorChain postInvocationExecutorChain, + final PostInvocationExecutorChain errorInvocationExecutorChain) { + this.preInvocationExecutorChain = preInvocationExecutorChain; + this.postInvocationExecutorChain = postInvocationExecutorChain; + this.errorInvocationExecutorChain = errorInvocationExecutorChain; + } + + @Override + public Object handle(final ProceedingJoinPoint joinPoint) throws Throwable { + + final SimplePreInvocationContext preContext = SimplePreInvocationContext.of(joinPoint); + final PostInvocationContext postContext = preInvocationExecutorChain.execute(preContext); + + if (postContext.wasSuccessful()) + return postInvocationExecutorChain.execute(postContext); + else + errorInvocationExecutorChain.execute(postContext); + + final Throwable throwable = postContext.getThrowable(); + if (throwable != null) + throw throwable; + + throw new IllegalStateException("Invocation failed but no throwable was provided."); + + } + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java new file mode 100644 index 00000000..0a30fbd4 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java @@ -0,0 +1,73 @@ +package com.callv2.aop.executor.chain.handler; + +import java.util.Objects; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.Executor; +import com.callv2.aop.executor.chain.PostInvocationExecutorChain; +import com.callv2.aop.executor.chain.PreInvocationExecutorChain; + +public class SimpleExecutorChainHandlerBuilder { + + private PreInvocationExecutorChain preInvocationExecutorChain; + private PostInvocationExecutorChain postInvocationExecutorChain; + private PostInvocationExecutorChain errorInvocationExecutorChain; + + public static SimpleExecutorChainHandlerBuilder create() { + return new SimpleExecutorChainHandlerBuilder(); + } + + public SimpleExecutorChainHandlerBuilder preExecutor(final Executor executor) { + + final PreInvocationExecutorChain preInvocationExecutorChain = new PreInvocationExecutorChain(executor); + + if (Objects.isNull(this.preInvocationExecutorChain)) { + this.preInvocationExecutorChain = preInvocationExecutorChain; + } else { + this.preInvocationExecutorChain = (PreInvocationExecutorChain) preInvocationExecutorChain + .setNext(preInvocationExecutorChain); + } + + return this; + + } + + public SimpleExecutorChainHandlerBuilder postExecutor(final Executor executor) { + + final PostInvocationExecutorChain postInvocationExecutorChain = new PostInvocationExecutorChain(executor); + + if (Objects.isNull(this.postInvocationExecutorChain)) { + this.postInvocationExecutorChain = postInvocationExecutorChain; + } else { + this.postInvocationExecutorChain = (PostInvocationExecutorChain) postInvocationExecutorChain + .setNext(postInvocationExecutorChain); + } + + return this; + + } + + public SimpleExecutorChainHandlerBuilder errorExecutor(final Executor executor) { + + final PostInvocationExecutorChain errorInvocationExecutorChain = new PostInvocationExecutorChain(executor); + + if (Objects.isNull(this.errorInvocationExecutorChain)) { + this.errorInvocationExecutorChain = errorInvocationExecutorChain; + } else { + this.errorInvocationExecutorChain = (PostInvocationExecutorChain) errorInvocationExecutorChain + .setNext(errorInvocationExecutorChain); + } + + return this; + + } + + public SimpleExecutorChainHandler build() { + return new SimpleExecutorChainHandler( + preInvocationExecutorChain, + postInvocationExecutorChain, + errorInvocationExecutorChain); + } + +} diff --git a/settings.gradle b/settings.gradle index 6ea23965..60ec077c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'drive-api' -include('domain', 'application', 'infrastructure') \ No newline at end of file +include('domain', 'application', 'infrastructure', 'aop') \ No newline at end of file From a8540fe4c7b8f8f9ba13da325887336f920e3062 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 3 Aug 2025 20:48:54 -0300 Subject: [PATCH 66/89] refactor: replace generic Executor with specific PostExecutor and PreExecutor in executor chains --- .../executor/chain/PostInvocationExecutorChain.java | 6 +++--- .../executor/chain/PreInvocationExecutorChain.java | 6 +++--- .../handler/SimpleExecutorChainHandlerBuilder.java | 11 +++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java index d5ebe7d5..0dedd463 100644 --- a/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java +++ b/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java @@ -1,12 +1,12 @@ package com.callv2.aop.executor.chain; import com.callv2.aop.context.PostInvocationContext; -import com.callv2.aop.executor.Executor; +import com.callv2.aop.executor.PostExecutor; public final class PostInvocationExecutorChain - extends ExecutorChain> { + extends ExecutorChain { - public PostInvocationExecutorChain(final Executor executor) { + public PostInvocationExecutorChain(final PostExecutor executor) { super(executor); } diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java index e2509df3..ed77a7df 100644 --- a/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java +++ b/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java @@ -2,12 +2,12 @@ import com.callv2.aop.context.PostInvocationContext; import com.callv2.aop.context.PreInvocationContext; -import com.callv2.aop.executor.Executor; +import com.callv2.aop.executor.PreExecutor; public class PreInvocationExecutorChain - extends ExecutorChain> { + extends ExecutorChain { - public PreInvocationExecutorChain(final Executor executor) { + public PreInvocationExecutorChain(final PreExecutor executor) { super(executor); } diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java index 0a30fbd4..45914c17 100644 --- a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java @@ -2,9 +2,8 @@ import java.util.Objects; -import com.callv2.aop.context.PostInvocationContext; -import com.callv2.aop.context.PreInvocationContext; -import com.callv2.aop.executor.Executor; +import com.callv2.aop.executor.PostExecutor; +import com.callv2.aop.executor.PreExecutor; import com.callv2.aop.executor.chain.PostInvocationExecutorChain; import com.callv2.aop.executor.chain.PreInvocationExecutorChain; @@ -18,7 +17,7 @@ public static SimpleExecutorChainHandlerBuilder create() { return new SimpleExecutorChainHandlerBuilder(); } - public SimpleExecutorChainHandlerBuilder preExecutor(final Executor executor) { + public SimpleExecutorChainHandlerBuilder preExecutor(final PreExecutor executor) { final PreInvocationExecutorChain preInvocationExecutorChain = new PreInvocationExecutorChain(executor); @@ -33,7 +32,7 @@ public SimpleExecutorChainHandlerBuilder preExecutor(final Executor executor) { + public SimpleExecutorChainHandlerBuilder postExecutor(final PostExecutor executor) { final PostInvocationExecutorChain postInvocationExecutorChain = new PostInvocationExecutorChain(executor); @@ -48,7 +47,7 @@ public SimpleExecutorChainHandlerBuilder postExecutor(final Executor executor) { + public SimpleExecutorChainHandlerBuilder errorExecutor(final PostExecutor executor) { final PostInvocationExecutorChain errorInvocationExecutorChain = new PostInvocationExecutorChain(executor); From e3361af3e70494dcd64bbb0658c95b2f025ae3fe Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 00:29:20 -0300 Subject: [PATCH 67/89] feat: Introduce InvocationContext and related classes for AOP execution context management - Added InvocationContext interface to encapsulate context information during method invocation. - Refactored PostInvocationContext and PreInvocationContext to extend InvocationContext. - Implemented SimplePreInvocationContext and SimplePostInvocationContext to handle invocation context. - Updated SimpleExecutorChainHandlerBuilder to manage pre and post invocation executor chains using queues. - Created ApplicationLayerAspect to handle method execution with AOP. - Removed obsolete aspect-related classes and interfaces to streamline the AOP implementation. - Introduced logging executors for method arguments, signatures, and execution telemetry. - Configured AOP beans in AopConfig for logging and error handling during method execution. --- .../context/AbstractInvocationContext.java | 19 +++- .../callv2/aop/context/InvocationContext.java | 13 +++ .../aop/context/PostInvocationContext.java | 4 +- .../aop/context/PreInvocationContext.java | 10 +- .../context/SimplePreInvocationContext.java | 18 --- .../SimpleExecutorChainHandlerBuilder.java | 83 ++++++++------ infrastructure/build.gradle | 1 + .../aop/aspect/ApplicationLayerAspect.java | 25 +++++ .../aspects/chain/AspectExecutorChain.java | 76 ------------- .../MethodInvocationAspectExecutorChain.java | 23 ---- .../PostInvocationAspectExecutorChain.java | 25 ----- .../aop/aspects/chain/Proceeder.java | 10 -- .../AbstractMethodInvocationContext.java | 27 ----- .../AbstractPostInvocationContext.java | 44 -------- .../context/MethodInvocationContext.java | 19 ---- .../context/PostInvocationContext.java | 20 ---- .../SimpleMethodInvocationContext.java | 64 ----------- .../context/SimplePostInvocationContext.java | 104 ------------------ .../aop/aspects/executor/ArgsLogExecutor.java | 24 ---- .../aop/aspects/executor/AspectExecutor.java | 10 -- .../executor/IdentifiableAspectExecutor.java | 11 -- .../executor/MethodSignatureLogExecutor.java | 18 --- .../executor/PostTelemetryLogExecutor.java | 22 ---- .../executor/ThrowableLogExecutor.java | 30 ----- ...leMethodInterceptorWithContextHandler.java | 51 --------- .../Log4jLogger.java} | 8 +- .../aop/executor/LogErrorPostExecutor.java | 30 +++++ .../executor/LogMethodArgsPreExecutor.java | 31 ++++++ .../LogMethodSignaturePreExecutor.java | 25 +++++ .../executor/LogTelemetryPostExecutor.java | 29 +++++ .../configuration/aop/AopConfig.java | 29 +++++ .../aop/aspect/AspectConfig.java | 65 ----------- .../aspect/PointcutAdvisorProperties.java | 48 -------- 33 files changed, 253 insertions(+), 763 deletions(-) create mode 100644 aop/src/main/java/com/callv2/aop/context/InvocationContext.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/AspectExecutorChain.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/MethodInvocationAspectExecutorChain.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/PostInvocationAspectExecutorChain.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/Proceeder.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractPostInvocationContext.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimpleMethodInvocationContext.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimplePostInvocationContext.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ArgsLogExecutor.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/AspectExecutor.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/IdentifiableAspectExecutor.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/MethodSignatureLogExecutor.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/PostTelemetryLogExecutor.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java rename infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/{aspects/executor/Log4jExecutor.java => executor/Log4jLogger.java} (79%) create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/AopConfig.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/aspect/AspectConfig.java delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/aspect/PointcutAdvisorProperties.java diff --git a/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java index e6f06f3e..b9ebd0dc 100644 --- a/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java +++ b/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java @@ -1,16 +1,23 @@ package com.callv2.aop.context; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; + import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.reflect.SourceLocation; import org.aspectj.runtime.internal.AroundClosure; -public abstract class AbstractInvocationContext implements ProceedingJoinPoint { +public abstract class AbstractInvocationContext implements InvocationContext { protected final ProceedingJoinPoint joinPoint; + protected final AtomicBoolean proceeded; + private final Instant contextedAt; protected AbstractInvocationContext(final ProceedingJoinPoint joinPoint) { this.joinPoint = joinPoint; + this.proceeded = new AtomicBoolean(false); + this.contextedAt = Instant.now(); } @Override @@ -63,4 +70,14 @@ public StaticPart getStaticPart() { return this.joinPoint.getStaticPart(); } + @Override + public Instant getContextedAt() { + return this.contextedAt; + } + + @Override + public boolean proceeded() { + return this.proceeded.get(); + } + } diff --git a/aop/src/main/java/com/callv2/aop/context/InvocationContext.java b/aop/src/main/java/com/callv2/aop/context/InvocationContext.java new file mode 100644 index 00000000..84e485fd --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/InvocationContext.java @@ -0,0 +1,13 @@ +package com.callv2.aop.context; + +import java.time.Instant; + +import org.aspectj.lang.ProceedingJoinPoint; + +public interface InvocationContext extends ProceedingJoinPoint { + + Instant getContextedAt(); + + boolean proceeded(); + +} diff --git a/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java index 74ef6ea8..f2a9f5b2 100644 --- a/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java +++ b/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java @@ -2,9 +2,7 @@ import java.time.Instant; -import org.aspectj.lang.ProceedingJoinPoint; - -public interface PostInvocationContext extends ProceedingJoinPoint { +public interface PostInvocationContext extends InvocationContext { Instant getProceededAt(); diff --git a/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java index 33387ab6..b2989833 100644 --- a/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java +++ b/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java @@ -1,14 +1,6 @@ package com.callv2.aop.context; -import java.time.Instant; - -import org.aspectj.lang.ProceedingJoinPoint; - -public interface PreInvocationContext extends ProceedingJoinPoint { - - Instant getContextedAt(); - - boolean proceeded(); +public interface PreInvocationContext extends InvocationContext { PostInvocationContext proceedWithContext(); diff --git a/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java index 45adc925..bbda8f5a 100644 --- a/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java +++ b/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java @@ -1,19 +1,11 @@ package com.callv2.aop.context; -import java.time.Instant; -import java.util.concurrent.atomic.AtomicBoolean; - import org.aspectj.lang.ProceedingJoinPoint; public final class SimplePreInvocationContext extends AbstractInvocationContext implements PreInvocationContext { - private final Instant contextedAt; - private final AtomicBoolean proceeded; - private SimplePreInvocationContext(final ProceedingJoinPoint joinPoint) { super(joinPoint); - this.contextedAt = Instant.now(); - this.proceeded = new AtomicBoolean(false); } public static SimplePreInvocationContext of(final ProceedingJoinPoint joinPoint) { @@ -34,16 +26,6 @@ public Object proceed(Object[] args) throws Throwable { return joinPoint.proceed(args); } - @Override - public Instant getContextedAt() { - return this.contextedAt; - } - - @Override - public boolean proceeded() { - return this.proceeded.get(); - } - @Override public PostInvocationContext proceedWithContext() { return SimplePostInvocationContext.from(this); diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java index 45914c17..cbba1e83 100644 --- a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java @@ -1,72 +1,83 @@ package com.callv2.aop.executor.chain.handler; -import java.util.Objects; +import java.util.Queue; +import org.aspectj.lang.ProceedingJoinPoint; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.Executor; import com.callv2.aop.executor.PostExecutor; import com.callv2.aop.executor.PreExecutor; +import com.callv2.aop.executor.chain.ExecutorChain; import com.callv2.aop.executor.chain.PostInvocationExecutorChain; import com.callv2.aop.executor.chain.PreInvocationExecutorChain; public class SimpleExecutorChainHandlerBuilder { - private PreInvocationExecutorChain preInvocationExecutorChain; - private PostInvocationExecutorChain postInvocationExecutorChain; - private PostInvocationExecutorChain errorInvocationExecutorChain; + private final Queue> preInvocationExecutorChainQueue; + private final Queue> postInvocationExecutorChainQueue; + private final Queue> errorInvocationExecutorChainQueue; + + public SimpleExecutorChainHandlerBuilder() { + this.preInvocationExecutorChainQueue = new java.util.LinkedList<>(); + this.postInvocationExecutorChainQueue = new java.util.LinkedList<>(); + this.errorInvocationExecutorChainQueue = new java.util.LinkedList<>(); + } public static SimpleExecutorChainHandlerBuilder create() { return new SimpleExecutorChainHandlerBuilder(); } public SimpleExecutorChainHandlerBuilder preExecutor(final PreExecutor executor) { - - final PreInvocationExecutorChain preInvocationExecutorChain = new PreInvocationExecutorChain(executor); - - if (Objects.isNull(this.preInvocationExecutorChain)) { - this.preInvocationExecutorChain = preInvocationExecutorChain; - } else { - this.preInvocationExecutorChain = (PreInvocationExecutorChain) preInvocationExecutorChain - .setNext(preInvocationExecutorChain); - } - + this.preInvocationExecutorChainQueue.add(new PreInvocationExecutorChain(executor)); return this; - } public SimpleExecutorChainHandlerBuilder postExecutor(final PostExecutor executor) { + this.postInvocationExecutorChainQueue.add(new PostInvocationExecutorChain(executor)); + return this; + } - final PostInvocationExecutorChain postInvocationExecutorChain = new PostInvocationExecutorChain(executor); - - if (Objects.isNull(this.postInvocationExecutorChain)) { - this.postInvocationExecutorChain = postInvocationExecutorChain; - } else { - this.postInvocationExecutorChain = (PostInvocationExecutorChain) postInvocationExecutorChain - .setNext(postInvocationExecutorChain); - } - + public SimpleExecutorChainHandlerBuilder errorExecutor(final PostExecutor executor) { + this.errorInvocationExecutorChainQueue.add(new PostInvocationExecutorChain(executor)); return this; + } + private static PreInvocationExecutorChain buildPreInvocationChain( + final Queue> chains) { + + return (PreInvocationExecutorChain) buildInvocationChain(chains); } - public SimpleExecutorChainHandlerBuilder errorExecutor(final PostExecutor executor) { + private PostInvocationExecutorChain buildPostInvocationChain( + Queue> chains) { - final PostInvocationExecutorChain errorInvocationExecutorChain = new PostInvocationExecutorChain(executor); + return (PostInvocationExecutorChain) buildInvocationChain(chains); + } - if (Objects.isNull(this.errorInvocationExecutorChain)) { - this.errorInvocationExecutorChain = errorInvocationExecutorChain; - } else { - this.errorInvocationExecutorChain = (PostInvocationExecutorChain) errorInvocationExecutorChain - .setNext(errorInvocationExecutorChain); - } + private static > ExecutorChain buildInvocationChain( + final Queue> chains) { + if (chains.isEmpty()) + return null; - return this; + final var firstChain = chains.poll(); + var chain = firstChain; + do { + final var next = chains.poll(); + chain = chain.setNext(next); + } while (!chains.isEmpty()); + + return firstChain; } public SimpleExecutorChainHandler build() { + return new SimpleExecutorChainHandler( - preInvocationExecutorChain, - postInvocationExecutorChain, - errorInvocationExecutorChain); + buildPreInvocationChain(preInvocationExecutorChainQueue), + buildPostInvocationChain(postInvocationExecutorChainQueue), + buildPostInvocationChain(errorInvocationExecutorChainQueue)); } } diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 5fadb191..c5ab35fc 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation(project(":domain")) implementation(project(":application")) + implementation(project(":aop")) implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java new file mode 100644 index 00000000..b7c241bc --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java @@ -0,0 +1,25 @@ +package com.callv2.drive.infrastructure.aop.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; + +@Aspect +@Component +public class ApplicationLayerAspect { + + private final ExecutorChainHandler chain; + + public ApplicationLayerAspect(final ExecutorChainHandler chain) { + this.chain = chain; + } + + @Around("execution(* com.callv2.drive.application..*.*(..))") + public Object aspect(ProceedingJoinPoint joinPoint) throws Throwable { + return chain.handle(joinPoint); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/AspectExecutorChain.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/AspectExecutorChain.java deleted file mode 100644 index b7aa2c67..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/AspectExecutorChain.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.chain; - -import java.util.ArrayDeque; -import java.util.Queue; - -import org.aopalliance.intercept.Joinpoint; - -import com.callv2.drive.infrastructure.aop.aspects.executor.AspectExecutor; - -public abstract class AspectExecutorChain> { - - private AspectExecutorChain next; - private final E executor; - - protected AspectExecutorChain(final E executor) { - this.executor = executor; - } - - public final O execute(final J joinpoint) throws Throwable { - - executor.execute(joinpoint); - - if (next != null) - return next.execute(joinpoint); - - return callsProceed(joinpoint); - } - - protected abstract O callsProceed(J joinpoint) throws Throwable; - - @SuppressWarnings("unchecked") - protected AspectExecutorChain setNext(final AspectExecutorChain next) { - return this.next = (AspectExecutorChain) next; - } - - public static final class Builder> { - - private final Class clazz; - private final Queue chains; - - private Builder(final Class clazz) { - this.clazz = clazz; - this.chains = new ArrayDeque<>(); - } - - public static > Builder create(final Class clazz) { - return new Builder(clazz); - } - - public Builder add(final C chain) { - - if (chain.getClass() != clazz) - throw new IllegalArgumentException("Chain must be exactly of type " + clazz.getName()); - - this.chains.add(chain); - return this; - } - - @SuppressWarnings("unchecked") - public C build() { - if (chains.isEmpty()) - return null; - - final var firstChain = chains.poll(); - var chain = firstChain; - - do { - chain = (C) chain.setNext(chains.poll()); - } while (!chains.isEmpty()); - - return firstChain; - } - - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/MethodInvocationAspectExecutorChain.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/MethodInvocationAspectExecutorChain.java deleted file mode 100644 index 2f0e954d..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/MethodInvocationAspectExecutorChain.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.chain; - -import com.callv2.drive.infrastructure.aop.aspects.context.MethodInvocationContext; -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; -import com.callv2.drive.infrastructure.aop.aspects.executor.AspectExecutor; - -public final class MethodInvocationAspectExecutorChain extends - AspectExecutorChain> { - - private MethodInvocationAspectExecutorChain(final AspectExecutor executor) { - super(executor); - } - - public static MethodInvocationAspectExecutorChain with(final AspectExecutor executor) { - return new MethodInvocationAspectExecutorChain(executor); - } - - @Override - protected PostInvocationContext callsProceed(final MethodInvocationContext joinpoint) throws Throwable { - return joinpoint.proceedWithContext(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/PostInvocationAspectExecutorChain.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/PostInvocationAspectExecutorChain.java deleted file mode 100644 index 8f4a953b..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/PostInvocationAspectExecutorChain.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.chain; - -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; -import com.callv2.drive.infrastructure.aop.aspects.executor.AspectExecutor; - -public final class PostInvocationAspectExecutorChain extends - AspectExecutorChain> { - - private PostInvocationAspectExecutorChain(final AspectExecutor executor) { - super(executor); - } - - public static PostInvocationAspectExecutorChain with(final AspectExecutor executor) { - return new PostInvocationAspectExecutorChain(executor); - } - - @Override - protected Object callsProceed(final PostInvocationContext joinpoint) throws Throwable { - if (joinpoint.wasSuccessful()) - return joinpoint.getResult(); - else - throw joinpoint.getThrowable(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/Proceeder.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/Proceeder.java deleted file mode 100644 index 1dad486c..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/Proceeder.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.chain; - -import org.aopalliance.intercept.Joinpoint; - -@FunctionalInterface -public interface Proceeder { - - O proceed(J joinpoint) throws Throwable; - -} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java deleted file mode 100644 index 325ab18f..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.time.Instant; - -import jakarta.annotation.Nonnull; - -public abstract class AbstractMethodInvocationContext implements MethodInvocationContext { - - private final Instant contextedAt; - - protected AbstractMethodInvocationContext() { - this.contextedAt = Instant.now(); - } - - @Override - @Nonnull - public Instant getContextedAt() { - return contextedAt; - } - - @Override - @Nonnull - public PostInvocationContext proceedWithContext() { - return SimplePostInvocationContext.captureFromExecution(this); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractPostInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractPostInvocationContext.java deleted file mode 100644 index bdeae37f..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractPostInvocationContext.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.time.Instant; -import java.util.concurrent.atomic.AtomicBoolean; - -public abstract class AbstractPostInvocationContext implements PostInvocationContext { - - private final Object result; - private final Throwable throwable; - private final Instant proceededAt; - private final AtomicBoolean successful; - - protected AbstractPostInvocationContext( - final Object result, - final Throwable throwable, - final Instant proceededAt, - final boolean successful) { - this.result = result; - this.throwable = throwable; - this.proceededAt = proceededAt; - this.successful = new AtomicBoolean(successful); - } - - @Override - public Instant getProceededAt() { - return proceededAt; - } - - @Override - public Object getResult() { - return result; - } - - @Override - public Throwable getThrowable() { - return throwable; - } - - @Override - public boolean wasSuccessful() { - return successful.get(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java deleted file mode 100644 index 0a5b46a6..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.time.Instant; - -import org.aopalliance.intercept.MethodInvocation; - -import jakarta.annotation.Nonnull; - -public interface MethodInvocationContext extends MethodInvocation { - - @Nonnull - Instant getContextedAt(); - - boolean proceeded(); - - @Nonnull - PostInvocationContext proceedWithContext(); - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java deleted file mode 100644 index 125b22de..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.time.Instant; - -import jakarta.annotation.Nullable; - -public interface PostInvocationContext extends MethodInvocationContext { - - @Nullable - Instant getProceededAt(); - - @Nullable - Object getResult(); - - @Nullable - Throwable getThrowable(); - - boolean wasSuccessful(); - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimpleMethodInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimpleMethodInvocationContext.java deleted file mode 100644 index b5e5d4ea..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimpleMethodInvocationContext.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.lang.reflect.AccessibleObject; -import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.aopalliance.intercept.MethodInvocation; - -public final class SimpleMethodInvocationContext extends AbstractMethodInvocationContext { - - private final AtomicBoolean proceeded; - private final MethodInvocation methodInvocation; - - private SimpleMethodInvocationContext(final MethodInvocation methodInvocation) { - super(); - this.proceeded = new AtomicBoolean(false); - this.methodInvocation = methodInvocation; - } - - public static SimpleMethodInvocationContext of(final MethodInvocation methodInvocation) { - return new SimpleMethodInvocationContext(methodInvocation); - } - - @Override - public boolean proceeded() { - return proceeded.get(); - } - - @Override - @Nonnull - public Method getMethod() { - return methodInvocation.getMethod(); - } - - @Override - @Nonnull - public Object[] getArguments() { - return methodInvocation.getArguments(); - } - - @Override - @Nullable - public Object proceed() throws Throwable { - if (proceeded.getAndSet(true)) - throw new IllegalStateException("Method already proceeded"); - return methodInvocation.proceed(); - } - - @Override - @Nullable - public Object getThis() { - return methodInvocation.getThis(); - } - - @Override - @Nonnull - public AccessibleObject getStaticPart() { - return methodInvocation.getStaticPart(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimplePostInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimplePostInvocationContext.java deleted file mode 100644 index 30b55dfc..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimplePostInvocationContext.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.lang.reflect.AccessibleObject; -import java.lang.reflect.Method; -import java.time.Instant; -import java.util.Objects; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public final class SimplePostInvocationContext extends AbstractPostInvocationContext { - - private final MethodInvocationContext methodInvocationContext; - - private SimplePostInvocationContext( - final Object result, - final Throwable throwable, - final Instant proceededAt, - final boolean successful, - final MethodInvocationContext methodInvocationContext) { - super(result, throwable, proceededAt, successful); - this.methodInvocationContext = Objects.requireNonNull(methodInvocationContext, - "'methodInvocationContext' must not be null"); - } - - public static final PostInvocationContext captureFromExecution( - final MethodInvocationContext methodInvocationContext) { - - Objects.requireNonNull(methodInvocationContext, "'methodInvocationContext' must not be null"); - - Object result; - Throwable throwable; - boolean successful; - final Instant proceededAt; - - try { - result = methodInvocationContext.proceed(); - throwable = null; - successful = true; - } catch (Throwable e) { - result = null; - throwable = e; - successful = false; - } finally { - proceededAt = Instant.now(); - } - - return new SimplePostInvocationContext( - result, - throwable, - proceededAt, - successful, - methodInvocationContext); - - } - - @Override - @Nonnull - public Instant getContextedAt() { - return methodInvocationContext.getContextedAt(); - } - - @Override - public boolean proceeded() { - return methodInvocationContext.proceeded(); - } - - @Override - @Nonnull - public PostInvocationContext proceedWithContext() { - return this; - } - - @Override - @Nonnull - public Method getMethod() { - return methodInvocationContext.getMethod(); - } - - @Override - @Nonnull - public Object[] getArguments() { - return methodInvocationContext.getArguments(); - } - - @Override - @Nullable - public Object proceed() throws Throwable { - return methodInvocationContext.proceed(); - } - - @Override - @Nullable - public Object getThis() { - return methodInvocationContext.getThis(); - } - - @Override - @Nonnull - public AccessibleObject getStaticPart() { - return methodInvocationContext.getStaticPart(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ArgsLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ArgsLogExecutor.java deleted file mode 100644 index 8519ed3e..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ArgsLogExecutor.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import java.util.Arrays; - -import org.apache.logging.log4j.Level; - -import com.callv2.drive.infrastructure.aop.aspects.context.MethodInvocationContext; - -public class ArgsLogExecutor extends Log4jExecutor { - - public ArgsLogExecutor(final Level level, final Class clazz) { - super(level, clazz); - } - - @Override - public void execute(final MethodInvocationContext context) { - final var args = context.getArguments(); - log("<> [{}] <> count:[{}] args: [{}]", - context.getMethod(), - args.length, - Arrays.toString(args)); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/AspectExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/AspectExecutor.java deleted file mode 100644 index 9b7cf5da..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/AspectExecutor.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import org.aopalliance.intercept.Joinpoint; - -@FunctionalInterface -public interface AspectExecutor { - - void execute(J joinPoint); - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/IdentifiableAspectExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/IdentifiableAspectExecutor.java deleted file mode 100644 index 8a96e709..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/IdentifiableAspectExecutor.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import org.aopalliance.intercept.Joinpoint; - -public interface IdentifiableAspectExecutor extends AspectExecutor { - - default String getId() { - return getClass().getSimpleName(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/MethodSignatureLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/MethodSignatureLogExecutor.java deleted file mode 100644 index 4c8ec16b..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/MethodSignatureLogExecutor.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import org.apache.logging.log4j.Level; - -import com.callv2.drive.infrastructure.aop.aspects.context.MethodInvocationContext; - -public class MethodSignatureLogExecutor extends Log4jExecutor { - - public MethodSignatureLogExecutor(final Level level, final Class clazz) { - super(level, clazz); - } - - @Override - public void execute(final MethodInvocationContext context) { - log("<>: [{}]", context.getMethod().toString()); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/PostTelemetryLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/PostTelemetryLogExecutor.java deleted file mode 100644 index 48675064..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/PostTelemetryLogExecutor.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import java.time.Duration; - -import org.apache.logging.log4j.Level; - -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; - -public class PostTelemetryLogExecutor extends Log4jExecutor { - - public PostTelemetryLogExecutor(final Level logLevel, final Class clazz) { - super(logLevel, clazz); - } - - @Override - public void execute(final PostInvocationContext context) { - log("<> [{}] ms <> [{}]", - Duration.between(context.getContextedAt(), context.getProceededAt()).toMillis(), - context.getMethod().toString()); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java deleted file mode 100644 index 72cebda8..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; -import org.apache.logging.log4j.Level; - -public class ThrowableLogExecutor extends Log4jExecutor { - - private ThrowableLogExecutor(final Level level, final Class clazz) { - super(level, clazz); - } - - public static ThrowableLogExecutor defaultCreate(final Class clazz) { - return new ThrowableLogExecutor(Level.ERROR, clazz); - } - - public static ThrowableLogExecutor create(final Level level, final Class clazz) { - return new ThrowableLogExecutor(level, clazz); - } - - @Override - public void execute(final PostInvocationContext context) { - if (context.getThrowable() != null) - log("<> [{}] <> [{}] <> [{}]", - context.getThrowable().getClass().getName(), - context.getThrowable().getMessage(), - context.getMethod().toString(), - context.getThrowable()); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java deleted file mode 100644 index 9c000fc3..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.handler; - -import javax.annotation.Nonnull; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; - -import com.callv2.drive.infrastructure.aop.aspects.chain.MethodInvocationAspectExecutorChain; -import com.callv2.drive.infrastructure.aop.aspects.chain.PostInvocationAspectExecutorChain; -import com.callv2.drive.infrastructure.aop.aspects.context.MethodInvocationContext; -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; -import com.callv2.drive.infrastructure.aop.aspects.context.SimpleMethodInvocationContext; - -import jakarta.annotation.Nullable; - -public final class SimpleMethodInterceptorWithContextHandler implements MethodInterceptor { - - private final MethodInvocationAspectExecutorChain beforeChain; - private final PostInvocationAspectExecutorChain afterChain; - private final PostInvocationAspectExecutorChain errorChain; - - public SimpleMethodInterceptorWithContextHandler( - final MethodInvocationAspectExecutorChain beforeChain, - final PostInvocationAspectExecutorChain afterChain, - final PostInvocationAspectExecutorChain errorChain) { - this.beforeChain = beforeChain; - this.afterChain = afterChain; - this.errorChain = errorChain; - } - - @Override - @Nullable - public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable { - - final MethodInvocationContext context = SimpleMethodInvocationContext.of(invocation); - - final PostInvocationContext postInvocationResult = beforeChain.execute(context); - - if (postInvocationResult.wasSuccessful()) - return afterChain.execute(postInvocationResult); - else - errorChain.execute(postInvocationResult); - - final Throwable throwable = postInvocationResult.getThrowable(); - if (throwable != null) - throw throwable; - - throw new IllegalStateException("Invocation failed but no throwable was provided."); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/Log4jExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java similarity index 79% rename from infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/Log4jExecutor.java rename to infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java index 16195a1f..e2a81d93 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/Log4jExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java @@ -1,18 +1,16 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; +package com.callv2.drive.infrastructure.aop.executor; -import org.aopalliance.intercept.Joinpoint; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.Message; -public abstract class Log4jExecutor implements IdentifiableAspectExecutor { +public class Log4jLogger { private final Logger logger; private final Level logLevel; - public Log4jExecutor(final Level logLevel, Class clazz) { - super(); + public Log4jLogger(final Level logLevel, final Class clazz) { this.logLevel = logLevel; this.logger = LogManager.getLogger(clazz); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java new file mode 100644 index 00000000..f77913ab --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java @@ -0,0 +1,30 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.executor.PostExecutor; + +public class LogErrorPostExecutor implements PostExecutor { + + private final Log4jLogger logger; + + public LogErrorPostExecutor(final Level logLevel) { + this.logger = new Log4jLogger(logLevel, LogTelemetryPostExecutor.class); + } + + public LogErrorPostExecutor(final Level logLevel, final Class clazz) { + this.logger = new Log4jLogger(logLevel, clazz); + } + + @Override + public void execute(final PostInvocationContext joinPoint) { + if (joinPoint.getThrowable() != null) + logger.log("<> [{}] <> [{}] <> [{}]", + joinPoint.getThrowable().getClass().getName(), + joinPoint.getThrowable().getMessage(), + joinPoint.getSignature().toString(), + joinPoint.getThrowable()); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java new file mode 100644 index 00000000..e4c30dee --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java @@ -0,0 +1,31 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import java.util.Arrays; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.PreExecutor; + +public class LogMethodArgsPreExecutor implements PreExecutor { + + private final Log4jLogger logger; + + public LogMethodArgsPreExecutor(final Level logLevel) { + this.logger = new Log4jLogger(logLevel, LogTelemetryPostExecutor.class); + } + + public LogMethodArgsPreExecutor(final Level logLevel, final Class clazz) { + this.logger = new Log4jLogger(logLevel, clazz); + } + + @Override + public void execute(final PreInvocationContext joinPoint) { + final var args = joinPoint.getArgs(); + logger.log("<> [{}] <> count:[{}] args: [{}]", + joinPoint.getSignature(), + args.length, + Arrays.toString(args)); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java new file mode 100644 index 00000000..5033b64b --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java @@ -0,0 +1,25 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.PreExecutor; + +public class LogMethodSignaturePreExecutor implements PreExecutor { + + private final Log4jLogger logger; + + public LogMethodSignaturePreExecutor(final Level logLevel) { + this.logger = new Log4jLogger(logLevel, LogTelemetryPostExecutor.class); + } + + public LogMethodSignaturePreExecutor(final Level logLevel, final Class clazz) { + this.logger = new Log4jLogger(logLevel, clazz); + } + + @Override + public void execute(final PreInvocationContext joinPoint) { + logger.log("<>: [{}]", joinPoint.getSignature().toString()); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java new file mode 100644 index 00000000..fbfd60a8 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java @@ -0,0 +1,29 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import java.time.Duration; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.executor.PostExecutor; + +public class LogTelemetryPostExecutor implements PostExecutor { + + private final Log4jLogger logger; + + public LogTelemetryPostExecutor(final Level logLevel) { + this.logger = new Log4jLogger(logLevel, LogTelemetryPostExecutor.class); + } + + public LogTelemetryPostExecutor(final Level logLevel, final Class clazz) { + this.logger = new Log4jLogger(logLevel, clazz); + } + + @Override + public void execute(final PostInvocationContext joinPoint) { + logger.log("<> [{}] ms <> [{}]", + Duration.between(joinPoint.getContextedAt(), joinPoint.getProceededAt()).toMillis(), + joinPoint.getSignature().toString()); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/AopConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/AopConfig.java new file mode 100644 index 00000000..33df1772 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/AopConfig.java @@ -0,0 +1,29 @@ +package com.callv2.drive.infrastructure.configuration.aop; + +import org.apache.logging.log4j.Level; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; +import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; +import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; + +@Configuration +public class AopConfig { + + @Bean + ExecutorChainHandler executorChainHandler() { + + return SimpleExecutorChainHandlerBuilder + .create() + .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) + .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) + .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) + .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) + .build(); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/aspect/AspectConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/aspect/AspectConfig.java deleted file mode 100644 index e76fca6c..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/aspect/AspectConfig.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.callv2.drive.infrastructure.configuration.aop.aspect; - -import org.aopalliance.intercept.MethodInterceptor; -import org.apache.logging.log4j.Level; -import org.springframework.aop.Advisor; -import org.springframework.aop.aspectj.AspectJExpressionPointcut; -import org.springframework.aop.support.DefaultPointcutAdvisor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.callv2.drive.infrastructure.aop.aspects.chain.AspectExecutorChain; -import com.callv2.drive.infrastructure.aop.aspects.chain.MethodInvocationAspectExecutorChain; -import com.callv2.drive.infrastructure.aop.aspects.chain.PostInvocationAspectExecutorChain; -import com.callv2.drive.infrastructure.aop.aspects.executor.ArgsLogExecutor; -import com.callv2.drive.infrastructure.aop.aspects.executor.MethodSignatureLogExecutor; -import com.callv2.drive.infrastructure.aop.aspects.executor.PostTelemetryLogExecutor; -import com.callv2.drive.infrastructure.aop.aspects.executor.ThrowableLogExecutor; -import com.callv2.drive.infrastructure.aop.aspects.handler.SimpleMethodInterceptorWithContextHandler; - -@Configuration -public class AspectConfig { - - @Bean - Advisor applicationAdvisor() { - - final var beforeChain = AspectExecutorChain.Builder - .create(MethodInvocationAspectExecutorChain.class) - .add(MethodInvocationAspectExecutorChain - .with(new MethodSignatureLogExecutor(Level.DEBUG, MethodSignatureLogExecutor.class))) - .add(MethodInvocationAspectExecutorChain - .with(new ArgsLogExecutor(Level.DEBUG, ArgsLogExecutor.class))) - .build(); - - final var afterChain = AspectExecutorChain.Builder - .create(PostInvocationAspectExecutorChain.class) - .add(PostInvocationAspectExecutorChain - .with(new PostTelemetryLogExecutor(Level.DEBUG, PostTelemetryLogExecutor.class))) - .build(); - - final var errorChain = AspectExecutorChain.Builder - .create(PostInvocationAspectExecutorChain.class) - .add(PostInvocationAspectExecutorChain - .with(new PostTelemetryLogExecutor(Level.ERROR, PostTelemetryLogExecutor.class))) - .add(PostInvocationAspectExecutorChain - .with(ThrowableLogExecutor.defaultCreate(ThrowableLogExecutor.class))) - .build(); - - return applicationAdvisor( - "execution(* com.callv2.drive.application..*.*(..))", - new SimpleMethodInterceptorWithContextHandler(beforeChain, afterChain, errorChain)); - } - - private static Advisor applicationAdvisor( - final String expression, - final MethodInterceptor interceptor) { - - final AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); - pointcut.setExpression(expression); - - return new DefaultPointcutAdvisor( - pointcut, - interceptor); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/aspect/PointcutAdvisorProperties.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/aspect/PointcutAdvisorProperties.java deleted file mode 100644 index c930113e..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/aspect/PointcutAdvisorProperties.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.callv2.drive.infrastructure.configuration.properties.aspect; - -import java.util.List; - -public class PointcutAdvisorProperties { - - private String expression; - - private List before; - private List after; - private List error; - - public PointcutAdvisorProperties() { - } - - public String getExpression() { - return expression; - } - - public void setExpression(String expression) { - this.expression = expression; - } - - public List getBefore() { - return before; - } - - public void setBefore(List before) { - this.before = before; - } - - public List getAfter() { - return after; - } - - public void setAfter(List after) { - this.after = after; - } - - public List getError() { - return error; - } - - public void setError(List error) { - this.error = error; - } - -} From ca6bce59ce29f816634fe9d9565fdd5e906ea0d3 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 00:49:32 -0300 Subject: [PATCH 68/89] fix: enhance AbstractInvocationContext to accept PreInvocationContext for improved context management --- .../com/callv2/aop/context/AbstractInvocationContext.java | 6 ++++++ .../com/callv2/aop/context/SimplePostInvocationContext.java | 6 ++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java index b9ebd0dc..5f16443c 100644 --- a/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java +++ b/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java @@ -20,6 +20,12 @@ protected AbstractInvocationContext(final ProceedingJoinPoint joinPoint) { this.contextedAt = Instant.now(); } + protected AbstractInvocationContext(final PreInvocationContext preInvocationContext) { + this.joinPoint = preInvocationContext; + this.proceeded = new AtomicBoolean(preInvocationContext.proceeded()); + this.contextedAt = preInvocationContext.getContextedAt(); + } + @Override public void set$AroundClosure(AroundClosure arc) { this.joinPoint.set$AroundClosure(arc); diff --git a/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java index 1ed55e3b..bddecadd 100644 --- a/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java +++ b/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java @@ -3,8 +3,6 @@ import java.time.Instant; import java.util.concurrent.atomic.AtomicBoolean; -import org.aspectj.lang.ProceedingJoinPoint; - public final class SimplePostInvocationContext extends AbstractInvocationContext implements PostInvocationContext { private final Object result; @@ -13,12 +11,12 @@ public final class SimplePostInvocationContext extends AbstractInvocationContext private final AtomicBoolean successful; private SimplePostInvocationContext( - final ProceedingJoinPoint joinPoint, + final PreInvocationContext preInvocationContext, final Object result, final Throwable throwable, final Instant proceededAt, final boolean successful) { - super(joinPoint); + super(preInvocationContext); this.result = result; this.throwable = throwable; this.proceededAt = proceededAt; From ce49b423b9c0b5d64d929592b2229dd89b22575e Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 11:37:25 -0300 Subject: [PATCH 69/89] refactor: consolidate AOP executor chain setup into ApplicationLayerAspect and remove AopConfig class --- .../aop/aspect/ApplicationLayerAspect.java | 20 +++++++++---- .../configuration/aop/AopConfig.java | 29 ------------------- 2 files changed, 14 insertions(+), 35 deletions(-) delete mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/AopConfig.java diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java index b7c241bc..11674904 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java @@ -1,24 +1,32 @@ package com.callv2.drive.infrastructure.aop.aspect; +import org.apache.logging.log4j.Level; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; +import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; +import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; @Aspect @Component public class ApplicationLayerAspect { - private final ExecutorChainHandler chain; - - public ApplicationLayerAspect(final ExecutorChainHandler chain) { - this.chain = chain; - } + private final ExecutorChainHandler chain = SimpleExecutorChainHandlerBuilder + .create() + .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) + .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) + .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) + .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) + .build(); @Around("execution(* com.callv2.drive.application..*.*(..))") - public Object aspect(ProceedingJoinPoint joinPoint) throws Throwable { + public Object aspect(final ProceedingJoinPoint joinPoint) throws Throwable { return chain.handle(joinPoint); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/AopConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/AopConfig.java deleted file mode 100644 index 33df1772..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/AopConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.callv2.drive.infrastructure.configuration.aop; - -import org.apache.logging.log4j.Level; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; -import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; -import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; -import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; -import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; -import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; - -@Configuration -public class AopConfig { - - @Bean - ExecutorChainHandler executorChainHandler() { - - return SimpleExecutorChainHandlerBuilder - .create() - .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) - .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) - .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) - .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) - .build(); - } - -} From cfd3e8a4ef25834a567aff9b855b4e09f9613bbe Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 11:50:12 -0300 Subject: [PATCH 70/89] fix: enhance proceed methods in SimplePostInvocationContext to return result or throw throwable based on success --- .../aop/context/SimplePostInvocationContext.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java index bddecadd..d150913b 100644 --- a/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java +++ b/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java @@ -53,11 +53,25 @@ protected static SimplePostInvocationContext from(final PreInvocationContext pre @Override public Object proceed() throws Throwable { + if (this.proceeded.get()) { + if (successful.get()) + return result; + else + throw throwable; + } + return this.joinPoint.proceed(); } @Override public Object proceed(Object[] args) throws Throwable { + if (this.proceeded.get()) { + if (successful.get()) + return result; + else + throw throwable; + } + return this.joinPoint.proceed(args); } From daf01c6fe714bdd45f200aa5f1a75e4a07eaa0c1 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 11:54:29 -0300 Subject: [PATCH 71/89] refactor: mark PreInvocationExecutorChain and SimpleExecutorChainHandlerBuilder as final to prevent subclassing --- .../callv2/aop/executor/chain/PreInvocationExecutorChain.java | 2 +- .../chain/handler/SimpleExecutorChainHandlerBuilder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java index ed77a7df..1f6e0b8f 100644 --- a/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java +++ b/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java @@ -4,7 +4,7 @@ import com.callv2.aop.context.PreInvocationContext; import com.callv2.aop.executor.PreExecutor; -public class PreInvocationExecutorChain +public final class PreInvocationExecutorChain extends ExecutorChain { public PreInvocationExecutorChain(final PreExecutor executor) { diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java index cbba1e83..6204e8c2 100644 --- a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java @@ -13,7 +13,7 @@ import com.callv2.aop.executor.chain.PostInvocationExecutorChain; import com.callv2.aop.executor.chain.PreInvocationExecutorChain; -public class SimpleExecutorChainHandlerBuilder { +public final class SimpleExecutorChainHandlerBuilder { private final Queue> preInvocationExecutorChainQueue; private final Queue> postInvocationExecutorChainQueue; From 608e21daf8ed0e915988bdecfc75bfccd94dee8f Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 19:34:21 -0300 Subject: [PATCH 72/89] feat: add DomainLayerAspect and InfrastructureLayerAspect for AOP execution handling; enhance ApplicationLayerAspect with result logging refactor: update logging in LogErrorPostExecutor, LogMethodArgsPreExecutor, LogMethodResultPostExecutor, LogMethodSignaturePreExecutor, and LogTelemetryPostExecutor for consistency fix: implement toString method in FileJpaEntity for better logging --- .../aop/aspect/ApplicationLayerAspect.java | 6 ++- .../aop/aspect/DomainLayerAspect.java | 35 ++++++++++++++++ .../aop/aspect/InfrastructureLayerAspect.java | 40 +++++++++++++++++++ .../aop/executor/LogErrorPostExecutor.java | 6 ++- .../executor/LogMethodArgsPreExecutor.java | 9 ++--- .../executor/LogMethodResultPostExecutor.java | 31 ++++++++++++++ .../LogMethodSignaturePreExecutor.java | 2 +- .../executor/LogTelemetryPostExecutor.java | 6 +-- .../converter/JacksonCaster.java | 5 +-- .../file/persistence/FileJpaEntity.java | 7 ++++ 10 files changed, 130 insertions(+), 17 deletions(-) create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java create mode 100644 infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java index 11674904..f803beaf 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java @@ -10,6 +10,7 @@ import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodResultPostExecutor; import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; @@ -17,17 +18,18 @@ @Component public class ApplicationLayerAspect { - private final ExecutorChainHandler chain = SimpleExecutorChainHandlerBuilder + private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder .create() .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) + .postExecutor(new LogMethodResultPostExecutor(Level.DEBUG)) .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) .build(); @Around("execution(* com.callv2.drive.application..*.*(..))") public Object aspect(final ProceedingJoinPoint joinPoint) throws Throwable { - return chain.handle(joinPoint); + return chainHandler.handle(joinPoint); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java new file mode 100644 index 00000000..c6b77392 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java @@ -0,0 +1,35 @@ +package com.callv2.drive.infrastructure.aop.aspect; + +import org.apache.logging.log4j.Level; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; +import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; +import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodResultPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; + +@Aspect +@Component +public class DomainLayerAspect { + + private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder + .create() + .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) + .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) + .postExecutor(new LogMethodResultPostExecutor(Level.DEBUG)) + .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) + .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) + .build(); + + @Around("execution(* com.callv2.drive.domain..*.*(..))") + public Object aspect(final ProceedingJoinPoint joinPoint) throws Throwable { + return chainHandler.handle(joinPoint); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java new file mode 100644 index 00000000..d846ada7 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java @@ -0,0 +1,40 @@ +package com.callv2.drive.infrastructure.aop.aspect; + +import org.apache.logging.log4j.Level; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; +import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; +import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodResultPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; + +@Aspect +@Component +public class InfrastructureLayerAspect { + + private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder + .create() + .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) + .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) + .postExecutor(new LogMethodResultPostExecutor(Level.DEBUG)) + .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) + .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) + .build(); + + @Around("execution(* com.callv2.drive.infrastructure.api.controller..*.*(..))") + public Object controllerAspect(final ProceedingJoinPoint joinPoint) throws Throwable { + return chainHandler.handle(joinPoint); + } + + @Around("execution(* com.callv2.drive.infrastructure.messaging..*.*(..))") + public Object messagingAspect(final ProceedingJoinPoint joinPoint) throws Throwable { + return chainHandler.handle(joinPoint); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java index f77913ab..4ea5adbd 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java @@ -20,11 +20,13 @@ public LogErrorPostExecutor(final Level logLevel, final Class clazz) { @Override public void execute(final PostInvocationContext joinPoint) { if (joinPoint.getThrowable() != null) - logger.log("<> [{}] <> [{}] <> [{}]", + logger.log("<> [{}] <> [{}] <> [{}]", + joinPoint.getSignature().toShortString(), joinPoint.getThrowable().getClass().getName(), joinPoint.getThrowable().getMessage(), - joinPoint.getSignature().toString(), joinPoint.getThrowable()); + else + logger.log("<> [{}] <>", joinPoint.getSignature().toShortString()); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java index e4c30dee..d18e3038 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java @@ -21,11 +21,10 @@ public LogMethodArgsPreExecutor(final Level logLevel, final Class clazz) { @Override public void execute(final PreInvocationContext joinPoint) { - final var args = joinPoint.getArgs(); - logger.log("<> [{}] <> count:[{}] args: [{}]", - joinPoint.getSignature(), - args.length, - Arrays.toString(args)); + + logger.log("<> [{}] <> [{}]", + joinPoint.getSignature().toShortString(), + Arrays.toString(joinPoint.getArgs())); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java new file mode 100644 index 00000000..cb3e1b1e --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java @@ -0,0 +1,31 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import java.util.Arrays; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.executor.PostExecutor; + +public class LogMethodResultPostExecutor implements PostExecutor { + + private final Log4jLogger logger; + + public LogMethodResultPostExecutor(final Level logLevel) { + this.logger = new Log4jLogger(logLevel, LogMethodResultPostExecutor.class); + } + + public LogMethodResultPostExecutor(final Level logLevel, final Class clazz) { + this.logger = new Log4jLogger(logLevel, clazz); + } + + @Override + public void execute(final PostInvocationContext joinPoint) { + + logger.log("<> [{}] <> [{}] <> [{}]", + joinPoint.getSignature().toShortString(), + Arrays.toString(joinPoint.getArgs()), + joinPoint.getResult()); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java index 5033b64b..fc83994f 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java @@ -19,7 +19,7 @@ public LogMethodSignaturePreExecutor(final Level logLevel, final Class clazz) @Override public void execute(final PreInvocationContext joinPoint) { - logger.log("<>: [{}]", joinPoint.getSignature().toString()); + logger.log("<>: [{}]", joinPoint.getSignature().toShortString()); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java index fbfd60a8..8285d3e2 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java @@ -21,9 +21,9 @@ public LogTelemetryPostExecutor(final Level logLevel, final Class clazz) { @Override public void execute(final PostInvocationContext joinPoint) { - logger.log("<> [{}] ms <> [{}]", - Duration.between(joinPoint.getContextedAt(), joinPoint.getProceededAt()).toMillis(), - joinPoint.getSignature().toString()); + logger.log("<>: [{}] <> [{}] ms", + joinPoint.getSignature().toShortString(), + Duration.between(joinPoint.getContextedAt(), joinPoint.getProceededAt()).toMillis()); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/converter/JacksonCaster.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/converter/JacksonCaster.java index d420a4f5..f7d0b9f7 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/converter/JacksonCaster.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/converter/JacksonCaster.java @@ -6,13 +6,10 @@ import com.callv2.drive.infrastructure.configuration.mapper.Mapper; @Component -public final class JacksonCaster implements Caster { +public class JacksonCaster implements Caster { private static final ObjectMapper mapper = Mapper.mapper(); - private JacksonCaster() { - } - @Override public T cast(Object value, Class targetType) { return mapper.convertValue(value, targetType); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/persistence/FileJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/persistence/FileJpaEntity.java index 2785ce24..bdb06259 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/persistence/FileJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/persistence/FileJpaEntity.java @@ -166,4 +166,11 @@ public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + @Override + public String toString() { + return "FileJpaEntity [id=" + id + ", ownerId=" + ownerId + ", folderId=" + folderId + ", name=" + name + + ", contentType=" + contentType + ", contentLocation=" + contentLocation + ", contentSize=" + + contentSize + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + "]"; + } + } From 5417029cf8909c3cc245b636a0acaeccf5b29ebd Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 23:31:47 -0300 Subject: [PATCH 73/89] fix: handle null cases in SimpleExecutorChainHandler constructor by providing no-op chains --- .../java/com/callv2/aop/executor/Executor.java | 1 + .../chain/handler/SimpleExecutorChainHandler.java | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/aop/src/main/java/com/callv2/aop/executor/Executor.java b/aop/src/main/java/com/callv2/aop/executor/Executor.java index 27458aef..f735e76e 100644 --- a/aop/src/main/java/com/callv2/aop/executor/Executor.java +++ b/aop/src/main/java/com/callv2/aop/executor/Executor.java @@ -2,6 +2,7 @@ import org.aspectj.lang.ProceedingJoinPoint; +@FunctionalInterface public interface Executor { void execute(J joinPoint); diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java index cc4b31d3..1d7033d8 100644 --- a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java @@ -13,13 +13,22 @@ public final class SimpleExecutorChainHandler implements ExecutorChainHandler { private final PostInvocationExecutorChain postInvocationExecutorChain; private final PostInvocationExecutorChain errorInvocationExecutorChain; + private final PreInvocationExecutorChain noOpPreInvocationExecutorChain = new PreInvocationExecutorChain(j -> { + }); + + private final PostInvocationExecutorChain noOpPostInvocationExecutorChain = new PostInvocationExecutorChain(j -> { + }); + public SimpleExecutorChainHandler( final PreInvocationExecutorChain preInvocationExecutorChain, final PostInvocationExecutorChain postInvocationExecutorChain, final PostInvocationExecutorChain errorInvocationExecutorChain) { - this.preInvocationExecutorChain = preInvocationExecutorChain; - this.postInvocationExecutorChain = postInvocationExecutorChain; - this.errorInvocationExecutorChain = errorInvocationExecutorChain; + this.preInvocationExecutorChain = preInvocationExecutorChain == null ? noOpPreInvocationExecutorChain + : preInvocationExecutorChain; + this.postInvocationExecutorChain = postInvocationExecutorChain == null ? noOpPostInvocationExecutorChain + : postInvocationExecutorChain; + this.errorInvocationExecutorChain = errorInvocationExecutorChain == null ? noOpPostInvocationExecutorChain + : errorInvocationExecutorChain; } @Override From 5252a47290ea172cfdb1d58ee857de229f7f8c6b Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 23:32:32 -0300 Subject: [PATCH 74/89] feat: implement toString methods for File, FileID, Folder, FolderID, Member, and MemberID classes for better logging and debugging --- .../src/main/java/com/callv2/drive/domain/file/File.java | 6 ++++++ .../src/main/java/com/callv2/drive/domain/file/FileID.java | 5 +++++ .../main/java/com/callv2/drive/domain/folder/Folder.java | 7 +++++++ .../main/java/com/callv2/drive/domain/folder/FolderID.java | 5 +++++ .../main/java/com/callv2/drive/domain/member/Member.java | 7 +++++++ .../main/java/com/callv2/drive/domain/member/MemberID.java | 5 +++++ 6 files changed, 35 insertions(+) diff --git a/domain/src/main/java/com/callv2/drive/domain/file/File.java b/domain/src/main/java/com/callv2/drive/domain/file/File.java index 99f8679d..cdfdd044 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/File.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/File.java @@ -136,4 +136,10 @@ private void selfValidate() { throw ValidationException.with("Validation fail has occoured", notification); } + @Override + public String toString() { + return "File [id=" + id + ", owner=" + owner + ", folder=" + folder + ", name=" + name + ", content=" + content + + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + "]"; + } + } \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileID.java b/domain/src/main/java/com/callv2/drive/domain/file/FileID.java index 9bd05546..3d8de895 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileID.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileID.java @@ -18,4 +18,9 @@ public static FileID unique() { return FileID.of(UUID.randomUUID()); } + @Override + public String toString() { + return "FileID [value=" + getValue() + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java b/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java index 35beb80d..9e1bc8fa 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java @@ -159,4 +159,11 @@ private void selfValidate() { throw ValidationException.with("Validation fail has occoured", notification); } + @Override + public String toString() { + return "Folder [id=" + id + ", rootFolder=" + rootFolder + ", owner=" + owner + ", name=" + name + + ", parentFolder=" + parentFolder + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + + ", deletedAt=" + deletedAt + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/FolderID.java b/domain/src/main/java/com/callv2/drive/domain/folder/FolderID.java index e5c4d642..eab47e85 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/FolderID.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/FolderID.java @@ -18,4 +18,9 @@ public static FolderID unique() { return FolderID.of(UUID.randomUUID()); } + @Override + public String toString() { + return "FolderID [value=" + getValue() + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index ff437a44..fd8529cd 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -172,4 +172,11 @@ public Long getSynchronizedVersion() { return synchronizedVersion; } + @Override + public String toString() { + return "Member [id=" + id + ", username=" + username + ", nickname=" + nickname + ", quota=" + quota + + ", quotaRequest=" + quotaRequest + ", hasSystemAccess=" + hasSystemAccess + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + ", synchronizedVersion=" + synchronizedVersion + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/MemberID.java b/domain/src/main/java/com/callv2/drive/domain/member/MemberID.java index 9310602a..2be9ec43 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/MemberID.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/MemberID.java @@ -12,4 +12,9 @@ public static MemberID of(final String id) { return new MemberID(id); } + @Override + public String toString() { + return "MemberID [value=" + getValue() + "]"; + } + } From 5efb228adc51b54b4960a375efcf3c46606c62f3 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 4 Aug 2025 23:46:07 -0300 Subject: [PATCH 75/89] fix: correct logger class reference in LogErrorPostExecutor, LogMethodArgsPreExecutor, and LogMethodSignaturePreExecutor constructors --- .../drive/infrastructure/aop/executor/LogErrorPostExecutor.java | 2 +- .../infrastructure/aop/executor/LogMethodArgsPreExecutor.java | 2 +- .../aop/executor/LogMethodSignaturePreExecutor.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java index 4ea5adbd..1ccbd6e1 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java @@ -10,7 +10,7 @@ public class LogErrorPostExecutor implements PostExecutor { private final Log4jLogger logger; public LogErrorPostExecutor(final Level logLevel) { - this.logger = new Log4jLogger(logLevel, LogTelemetryPostExecutor.class); + this.logger = new Log4jLogger(logLevel, LogErrorPostExecutor.class); } public LogErrorPostExecutor(final Level logLevel, final Class clazz) { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java index d18e3038..b833443e 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java @@ -12,7 +12,7 @@ public class LogMethodArgsPreExecutor implements PreExecutor { private final Log4jLogger logger; public LogMethodArgsPreExecutor(final Level logLevel) { - this.logger = new Log4jLogger(logLevel, LogTelemetryPostExecutor.class); + this.logger = new Log4jLogger(logLevel, LogMethodArgsPreExecutor.class); } public LogMethodArgsPreExecutor(final Level logLevel, final Class clazz) { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java index fc83994f..84c10f49 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java @@ -10,7 +10,7 @@ public class LogMethodSignaturePreExecutor implements PreExecutor { private final Log4jLogger logger; public LogMethodSignaturePreExecutor(final Level logLevel) { - this.logger = new Log4jLogger(logLevel, LogTelemetryPostExecutor.class); + this.logger = new Log4jLogger(logLevel, LogMethodSignaturePreExecutor.class); } public LogMethodSignaturePreExecutor(final Level logLevel, final Class clazz) { From c8df211b1896645b0526c7d6d513aca6287c7dbb Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 23 Aug 2025 18:00:51 -0300 Subject: [PATCH 76/89] refactor: update executor classes to use static factory methods for instantiation and extend Log4jLogger --- .../aop/aspect/ApplicationLayerAspect.java | 10 +++++----- .../aop/aspect/DomainLayerAspect.java | 10 +++++----- .../aop/aspect/InfrastructureLayerAspect.java | 10 +++++----- .../infrastructure/aop/executor/Log4jLogger.java | 4 ++-- .../aop/executor/LogErrorPostExecutor.java | 16 +++++++--------- .../aop/executor/LogMethodArgsPreExecutor.java | 14 ++++++-------- .../executor/LogMethodResultPostExecutor.java | 14 ++++++-------- .../executor/LogMethodSignaturePreExecutor.java | 14 ++++++-------- .../aop/executor/LogTelemetryPostExecutor.java | 14 ++++++-------- 9 files changed, 48 insertions(+), 58 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java index f803beaf..4952cc9a 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java @@ -20,11 +20,11 @@ public class ApplicationLayerAspect { private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder .create() - .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) - .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) - .postExecutor(new LogMethodResultPostExecutor(Level.DEBUG)) - .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) - .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) + .preExecutor(LogMethodSignaturePreExecutor.create(Level.INFO)) + .preExecutor(LogMethodArgsPreExecutor.create(Level.DEBUG)) + .postExecutor(LogMethodResultPostExecutor.create(Level.DEBUG)) + .postExecutor(LogTelemetryPostExecutor.create(Level.INFO)) + .errorExecutor(LogErrorPostExecutor.create(Level.ERROR)) .build(); @Around("execution(* com.callv2.drive.application..*.*(..))") diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java index c6b77392..9a53f1b5 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java @@ -20,11 +20,11 @@ public class DomainLayerAspect { private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder .create() - .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) - .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) - .postExecutor(new LogMethodResultPostExecutor(Level.DEBUG)) - .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) - .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) + .preExecutor(LogMethodSignaturePreExecutor.create(Level.INFO)) + .preExecutor(LogMethodArgsPreExecutor.create(Level.DEBUG)) + .postExecutor(LogMethodResultPostExecutor.create(Level.DEBUG)) + .postExecutor(LogTelemetryPostExecutor.create(Level.INFO)) + .errorExecutor(LogErrorPostExecutor.create(Level.ERROR)) .build(); @Around("execution(* com.callv2.drive.domain..*.*(..))") diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java index d846ada7..8cfce871 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java @@ -20,11 +20,11 @@ public class InfrastructureLayerAspect { private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder .create() - .preExecutor(new LogMethodSignaturePreExecutor(Level.INFO)) - .preExecutor(new LogMethodArgsPreExecutor(Level.DEBUG)) - .postExecutor(new LogMethodResultPostExecutor(Level.DEBUG)) - .postExecutor(new LogTelemetryPostExecutor(Level.INFO)) - .errorExecutor(new LogErrorPostExecutor(Level.ERROR)) + .preExecutor(LogMethodSignaturePreExecutor.create(Level.INFO)) + .preExecutor(LogMethodArgsPreExecutor.create(Level.DEBUG)) + .postExecutor(LogMethodResultPostExecutor.create(Level.DEBUG)) + .postExecutor(LogTelemetryPostExecutor.create(Level.INFO)) + .errorExecutor(LogErrorPostExecutor.create(Level.ERROR)) .build(); @Around("execution(* com.callv2.drive.infrastructure.api.controller..*.*(..))") diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java index e2a81d93..b4303d38 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java @@ -5,12 +5,12 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.Message; -public class Log4jLogger { +public abstract class Log4jLogger { private final Logger logger; private final Level logLevel; - public Log4jLogger(final Level logLevel, final Class clazz) { + protected Log4jLogger(final Level logLevel, final Class clazz) { this.logLevel = logLevel; this.logger = LogManager.getLogger(clazz); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java index 1ccbd6e1..943d3258 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java @@ -5,28 +5,26 @@ import com.callv2.aop.context.PostInvocationContext; import com.callv2.aop.executor.PostExecutor; -public class LogErrorPostExecutor implements PostExecutor { +public class LogErrorPostExecutor extends Log4jLogger implements PostExecutor { - private final Log4jLogger logger; - - public LogErrorPostExecutor(final Level logLevel) { - this.logger = new Log4jLogger(logLevel, LogErrorPostExecutor.class); + private LogErrorPostExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); } - public LogErrorPostExecutor(final Level logLevel, final Class clazz) { - this.logger = new Log4jLogger(logLevel, clazz); + public static LogErrorPostExecutor create(final Level logLevel) { + return new LogErrorPostExecutor(logLevel, LogErrorPostExecutor.class); } @Override public void execute(final PostInvocationContext joinPoint) { if (joinPoint.getThrowable() != null) - logger.log("<> [{}] <> [{}] <> [{}]", + log("<> [{}] <> [{}] <> [{}]", joinPoint.getSignature().toShortString(), joinPoint.getThrowable().getClass().getName(), joinPoint.getThrowable().getMessage(), joinPoint.getThrowable()); else - logger.log("<> [{}] <>", joinPoint.getSignature().toShortString()); + log("<> [{}] <>", joinPoint.getSignature().toShortString()); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java index b833443e..705dbf1b 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java @@ -7,22 +7,20 @@ import com.callv2.aop.context.PreInvocationContext; import com.callv2.aop.executor.PreExecutor; -public class LogMethodArgsPreExecutor implements PreExecutor { +public class LogMethodArgsPreExecutor extends Log4jLogger implements PreExecutor { - private final Log4jLogger logger; - - public LogMethodArgsPreExecutor(final Level logLevel) { - this.logger = new Log4jLogger(logLevel, LogMethodArgsPreExecutor.class); + private LogMethodArgsPreExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); } - public LogMethodArgsPreExecutor(final Level logLevel, final Class clazz) { - this.logger = new Log4jLogger(logLevel, clazz); + public static LogMethodArgsPreExecutor create(final Level logLevel) { + return new LogMethodArgsPreExecutor(logLevel, LogMethodArgsPreExecutor.class); } @Override public void execute(final PreInvocationContext joinPoint) { - logger.log("<> [{}] <> [{}]", + log("<> [{}] <> [{}]", joinPoint.getSignature().toShortString(), Arrays.toString(joinPoint.getArgs())); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java index cb3e1b1e..99ba0811 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java @@ -7,22 +7,20 @@ import com.callv2.aop.context.PostInvocationContext; import com.callv2.aop.executor.PostExecutor; -public class LogMethodResultPostExecutor implements PostExecutor { +public class LogMethodResultPostExecutor extends Log4jLogger implements PostExecutor { - private final Log4jLogger logger; - - public LogMethodResultPostExecutor(final Level logLevel) { - this.logger = new Log4jLogger(logLevel, LogMethodResultPostExecutor.class); + private LogMethodResultPostExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); } - public LogMethodResultPostExecutor(final Level logLevel, final Class clazz) { - this.logger = new Log4jLogger(logLevel, clazz); + public static LogMethodResultPostExecutor create(final Level logLevel) { + return new LogMethodResultPostExecutor(logLevel, LogMethodResultPostExecutor.class); } @Override public void execute(final PostInvocationContext joinPoint) { - logger.log("<> [{}] <> [{}] <> [{}]", + log("<> [{}] <> [{}] <> [{}]", joinPoint.getSignature().toShortString(), Arrays.toString(joinPoint.getArgs()), joinPoint.getResult()); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java index 84c10f49..90fef616 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java @@ -5,21 +5,19 @@ import com.callv2.aop.context.PreInvocationContext; import com.callv2.aop.executor.PreExecutor; -public class LogMethodSignaturePreExecutor implements PreExecutor { +public class LogMethodSignaturePreExecutor extends Log4jLogger implements PreExecutor { - private final Log4jLogger logger; - - public LogMethodSignaturePreExecutor(final Level logLevel) { - this.logger = new Log4jLogger(logLevel, LogMethodSignaturePreExecutor.class); + private LogMethodSignaturePreExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); } - public LogMethodSignaturePreExecutor(final Level logLevel, final Class clazz) { - this.logger = new Log4jLogger(logLevel, clazz); + public static LogMethodSignaturePreExecutor create(final Level logLevel) { + return new LogMethodSignaturePreExecutor(logLevel, LogMethodSignaturePreExecutor.class); } @Override public void execute(final PreInvocationContext joinPoint) { - logger.log("<>: [{}]", joinPoint.getSignature().toShortString()); + log("<>: [{}]", joinPoint.getSignature().toShortString()); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java index 8285d3e2..1bc4769b 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java @@ -7,21 +7,19 @@ import com.callv2.aop.context.PostInvocationContext; import com.callv2.aop.executor.PostExecutor; -public class LogTelemetryPostExecutor implements PostExecutor { +public class LogTelemetryPostExecutor extends Log4jLogger implements PostExecutor { - private final Log4jLogger logger; - - public LogTelemetryPostExecutor(final Level logLevel) { - this.logger = new Log4jLogger(logLevel, LogTelemetryPostExecutor.class); + private LogTelemetryPostExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); } - public LogTelemetryPostExecutor(final Level logLevel, final Class clazz) { - this.logger = new Log4jLogger(logLevel, clazz); + public static LogTelemetryPostExecutor create(final Level logLevel) { + return new LogTelemetryPostExecutor(logLevel, LogTelemetryPostExecutor.class); } @Override public void execute(final PostInvocationContext joinPoint) { - logger.log("<>: [{}] <> [{}] ms", + log("<>: [{}] <> [{}] ms", joinPoint.getSignature().toShortString(), Duration.between(joinPoint.getContextedAt(), joinPoint.getProceededAt()).toMillis()); } From eeb9b0d0f1c95e9d35ef035b88f61d042aabebdf Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 23 Aug 2025 23:08:06 -0300 Subject: [PATCH 77/89] refactor: streamline folder name update logic and improve validation checks --- .../name/DefaultUpdateFolderNameUseCase.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java index bbdb4fd2..d3267a16 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java @@ -1,5 +1,7 @@ package com.callv2.drive.application.folder.update.name; +import java.util.Set; + import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.folder.Folder; @@ -24,25 +26,19 @@ public void execute(final UpdateFolderNameInput input) { .findById(FolderID.of(input.folderId())) .orElseThrow(() -> NotFoundException.with(Folder.class, input.folderId().toString())); - final Folder parentFolder = this.folderGateway - .findById(folder.getParentFolder()) - .orElseThrow( - () -> NotFoundException.with(Folder.class, folder.getParentFolder().getValue().toString())); - final FolderName folderName = FolderName.of(input.name()); - validateFolderName(parentFolder, folderName); - - this.folderGateway.update(folder.changeName(folderName)); - } - - private void validateFolderName(final Folder parentFolder, final FolderName name) { final Notification notification = Notification.create(); - if (parentFolder.getSubFolders().stream().anyMatch(subFolder -> subFolder.name().equals(name))) + final Set subFolders = folderGateway.findByParentFolderId(folder.getParentFolder()); + + if (subFolders.stream().anyMatch(subFolder -> subFolder.getName().equals(folderName))) notification.append(ValidationError.with("Folder with the same name already exists")); if (notification.hasError()) throw ValidationException.with("Could not update folder name", notification); + + this.folderGateway.update(folder.changeName(folderName)); } + } \ No newline at end of file From 2c31098ba3c4a554987a128275e92ee17647fc2a Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 23 Aug 2025 23:08:25 -0300 Subject: [PATCH 78/89] refactor: update endpoint for changing folder name to use a simplified path --- .../java/com/callv2/drive/infrastructure/api/FolderAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java index 26b532e6..98ef6738 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java @@ -88,7 +88,7 @@ ResponseEntity> list( @RequestParam(name = "filterOperator", required = false, defaultValue = "AND") Filter.Operator filterOperator, @RequestParam(name = "filters", required = false) List filters); - @PatchMapping(value = "{id}/change-name", consumes = { MediaType.APPLICATION_JSON_VALUE }) + @PatchMapping(value = "{id}/name", consumes = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Change folder name", description = "This method changes the name of a folder", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses({ @ApiResponse(responseCode = "204", description = "Folder name changed successfully", content = @Content(schema = @Schema(implementation = Void.class))), From 7fd4e4279326237a6079c0787050df8c42854157 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 23 Aug 2025 23:10:45 -0300 Subject: [PATCH 79/89] refactor: update endpoint for moving a folder to use a more descriptive path --- .../java/com/callv2/drive/infrastructure/api/FolderAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java index 98ef6738..82c55ef4 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java @@ -62,7 +62,7 @@ public interface FolderAPI { }) ResponseEntity getById(@PathVariable(required = true) UUID id); - @PatchMapping(value = "{id}/move", consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { + @PatchMapping(value = "{id}/parent", consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Move a folder", description = "This method moves a folder to a new location", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses({ From cfe156dc4f51e664bca08aaa4d647d9d101d036e Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sat, 23 Aug 2025 23:34:31 -0300 Subject: [PATCH 80/89] refactor: clean up API annotations and improve response documentation --- .../drive/infrastructure/api/FileAPI.java | 91 ++++++++----------- .../drive/infrastructure/api/FolderAPI.java | 66 +++++--------- .../drive/infrastructure/api/MemberAPI.java | 17 ++-- .../infrastructure/api/MemberAdminAPI.java | 28 ++---- 4 files changed, 80 insertions(+), 122 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java index 6b8799fa..01ca14b3 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java @@ -4,7 +4,6 @@ import java.util.UUID; import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -27,7 +26,6 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -35,59 +33,48 @@ @RequestMapping("files") public interface FileAPI { - @PostMapping(value = "/folders/{folderId}/upload", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE }, produces = { - MediaType.APPLICATION_JSON_VALUE }) - @Operation(summary = "Upload a file to a specific folder", description = "This method uploads a file", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "File uploaded successfully", content = @Content(schema = @Schema(implementation = CreateFileResponse.class))), - @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "413", description = "File is too large", content = @Content(schema = @Schema(implementation = ApiError.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity create( - @PathVariable(required = true, name = "folderId") UUID folderId, - @RequestPart("file") MultipartFile file); + @Operation(summary = "Upload a file to a specific folder", description = "This method uploads a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "201", description = "File uploaded successfully", content = @Content(schema = @Schema(implementation = CreateFileResponse.class))) + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "413", description = "File is too large", content = @Content(schema = @Schema(implementation = ApiError.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @PostMapping("/folders/{folderId}/upload") + ResponseEntity create( + @PathVariable(required = true, name = "folderId") UUID folderId, + @RequestPart("file") MultipartFile file); - @DeleteMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) - @Operation(summary = "Delete a file", description = "This method deletes a file", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "File deleted successfully"), - @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity delete(@PathVariable(required = true) UUID id); + @Operation(summary = "Delete a file", description = "This method deletes a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "204", description = "File deleted successfully") + @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @DeleteMapping("{id}") + ResponseEntity delete(@PathVariable(required = true) UUID id); - @GetMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) - @Operation(summary = "Retrive a file", description = "This method retrive a file", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "File retrieved successfully", content = @Content(schema = @Schema(implementation = GetFileResponse.class))), - @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity getById(@PathVariable(required = true) UUID id); + @Operation(summary = "Retrive a file", description = "This method retrive a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "200", description = "File retrieved successfully", content = @Content(schema = @Schema(implementation = GetFileResponse.class))) + @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("{id}") + ResponseEntity getById(@PathVariable(required = true) UUID id); - @GetMapping(value = "{id}/download", produces = { MediaType.APPLICATION_OCTET_STREAM_VALUE }) - @Operation(summary = "Download a file", description = "This method downloads a file", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "File downloaded successfully", content = @Content(schema = @Schema(implementation = Resource.class))), - @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity download(@PathVariable(required = true) UUID id); + @Operation(summary = "Download a file", description = "This method downloads a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "200", description = "File downloaded successfully", content = @Content(schema = @Schema(implementation = Resource.class))) + @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("{id}/download") + ResponseEntity download(@PathVariable(required = true) UUID id); - @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE }) - @Operation(summary = "List files", description = "This method list files", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Files listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { - FileListResponse.class }))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity> list( - @RequestParam(name = "page", required = false, defaultValue = "0") final int page, - @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, - @RequestParam(name = "orderField", required = false, defaultValue = "createdAt") String orderField, - @RequestParam(name = "orderDirection", required = false, defaultValue = "DESC") Pagination.Order.Direction orderDirection, - @RequestParam(name = "filterOperator", required = false, defaultValue = "AND") Filter.Operator filterOperator, - @RequestParam(name = "filters", required = false) List filters); + @Operation(summary = "List files", description = "This method list files", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "200", description = "Files listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { + FileListResponse.class }))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping + ResponseEntity> list( + @RequestParam(name = "page", required = false, defaultValue = "0") final int page, + @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, + @RequestParam(name = "orderField", required = false, defaultValue = "createdAt") String orderField, + @RequestParam(name = "orderDirection", required = false, defaultValue = "DESC") Pagination.Order.Direction orderDirection, + @RequestParam(name = "filterOperator", required = false, defaultValue = "AND") Filter.Operator filterOperator, + @RequestParam(name = "filters", required = false) List filters); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java index 82c55ef4..0c5a9817 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.UUID; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -27,7 +26,6 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -35,51 +33,39 @@ @RequestMapping("folders") public interface FolderAPI { - @GetMapping(value = "root", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a folder", description = "This method retrive a root folder", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Root folder retrieved successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "200", description = "Root folder retrieved successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("root") ResponseEntity getRoot(); - @PostMapping(consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { - MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Create a folder", description = "This method creates a folder", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "Folder created successfully", content = @Content(schema = @Schema(implementation = CreateFolderResponse.class))), - @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "201", description = "Folder created successfully", content = @Content(schema = @Schema(implementation = CreateFolderResponse.class))) + @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @PostMapping ResponseEntity create(@RequestBody CreateFolderRequest request); - @GetMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a folder", description = "This method retrive a folder", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Folder retrieved successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), - @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "200", description = "Folder retrieved successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))) + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("{id}") ResponseEntity getById(@PathVariable(required = true) UUID id); - @PatchMapping(value = "{id}/parent", consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { - MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Move a folder", description = "This method moves a folder to a new location", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "Folder moved successfully", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "204", description = "Folder moved successfully", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @PatchMapping("{id}/parent") ResponseEntity move(@PathVariable(required = true) UUID id, @RequestBody MoveFolderRequest request); - @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "List folders", description = "This method list folders", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Folders listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { - FolderListResponse.class }))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "200", description = "Folders listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { + FolderListResponse.class }))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping ResponseEntity> list( @RequestParam(name = "page", required = false, defaultValue = "0") final int page, @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, @@ -88,14 +74,12 @@ ResponseEntity> list( @RequestParam(name = "filterOperator", required = false, defaultValue = "AND") Filter.Operator filterOperator, @RequestParam(name = "filters", required = false) List filters); - @PatchMapping(value = "{id}/name", consumes = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Change folder name", description = "This method changes the name of a folder", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "Folder name changed successfully", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "204", description = "Folder name changed successfully", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @PatchMapping("{id}/name") ResponseEntity changeName(@PathVariable(required = true) UUID id, @RequestBody String request); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAPI.java index c7e48c99..da733ae1 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAPI.java @@ -14,7 +14,6 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -22,22 +21,18 @@ @RequestMapping("members") public interface MemberAPI { - @PostMapping("quotas/requests/{amount}") @Operation(summary = "Request drive quota", description = "This method request a drive ammount quota", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "Requested successfuly"), - @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - }) + @ApiResponse(responseCode = "204", description = "Requested successfuly") + @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) + @PostMapping("quotas/requests/{amount}") ResponseEntity requestQuota( @PathVariable(value = "amount", required = true) long amount, @RequestParam(value = "unit", defaultValue = "GIGABYTE") QuotaUnit unit); - @GetMapping("quotas") @Operation(summary = "Retrieve actual drive quota", description = "This method retrieve a drive ammount quota", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Retrieve successfuly"), - @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - }) + @ApiResponse(responseCode = "200", description = "Retrieve successfuly") + @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) + @GetMapping("quotas") ResponseEntity getQuota(); } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java index 544dd3ec..e2f1884e 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java @@ -1,6 +1,5 @@ package com.callv2.drive.infrastructure.api; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -18,7 +17,6 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,31 +24,25 @@ @RequestMapping("admin/members") public interface MemberAdminAPI { - @GetMapping("{id}/quotas") @Operation(summary = "Request drive quota", description = "This method request a drive ammount quota", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Retrieve successfuly"), - @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - }) + @ApiResponse(responseCode = "200", description = "Retrieve successfuly") + @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) + @GetMapping("{id}/quotas") ResponseEntity getQuota(@PathVariable(value = "id", required = true) String id); - @PostMapping("{id}/quotas/requests/approve") @Operation(summary = "Approve drive quota request", description = "This method approve a drive ammount quota request", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "Approved successfuly"), - @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - }) + @ApiResponse(responseCode = "204", description = "Approved successfuly") + @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) + @PostMapping("{id}/quotas/requests/approve") ResponseEntity approveQuotaRequest( @PathVariable(value = "id", required = true) String id, @RequestParam(value = "approved", defaultValue = "true") boolean approved); - @GetMapping(value = "quotas/requests", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "List quotas requests", description = "This method list quotas requests", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Quotas requests listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { - QuotaRequestListResponse.class }))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "200", description = "Quotas requests listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { + QuotaRequestListResponse.class }))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("quotas/requests") ResponseEntity> listQuotaRequests( @RequestParam(name = "page", required = false, defaultValue = "0") final int page, @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, From dc25f2984f25baa87c0b913ba3038c35265334c2 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 24 Aug 2025 18:48:09 -0300 Subject: [PATCH 81/89] refactor: update FileAPI to use @RequestParam for folderId and change MemberAdminAPI to use @PatchMapping for quota approval --- .../callv2/drive/infrastructure/api/FileAPI.java | 16 ++++++++-------- .../drive/infrastructure/api/MemberAdminAPI.java | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java index 01ca14b3..9f61d40f 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java @@ -4,6 +4,7 @@ import java.util.UUID; import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -11,7 +12,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; import com.callv2.drive.domain.pagination.Filter; @@ -38,31 +38,31 @@ public interface FileAPI { @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) @ApiResponse(responseCode = "413", description = "File is too large", content = @Content(schema = @Schema(implementation = ApiError.class))) @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - @PostMapping("/folders/{folderId}/upload") + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) ResponseEntity create( - @PathVariable(required = true, name = "folderId") UUID folderId, - @RequestPart("file") MultipartFile file); + @RequestParam("folderId") UUID folderId, + @RequestParam("file") MultipartFile file); @Operation(summary = "Delete a file", description = "This method deletes a file", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponse(responseCode = "204", description = "File deleted successfully") @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) @DeleteMapping("{id}") - ResponseEntity delete(@PathVariable(required = true) UUID id); + ResponseEntity delete(@PathVariable UUID id); @Operation(summary = "Retrive a file", description = "This method retrive a file", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponse(responseCode = "200", description = "File retrieved successfully", content = @Content(schema = @Schema(implementation = GetFileResponse.class))) @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) @GetMapping("{id}") - ResponseEntity getById(@PathVariable(required = true) UUID id); + ResponseEntity getById(@PathVariable UUID id); @Operation(summary = "Download a file", description = "This method downloads a file", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponse(responseCode = "200", description = "File downloaded successfully", content = @Content(schema = @Schema(implementation = Resource.class))) @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - @GetMapping("{id}/download") - ResponseEntity download(@PathVariable(required = true) UUID id); + @GetMapping("{id}/content") + ResponseEntity download(@PathVariable UUID id); @Operation(summary = "List files", description = "This method list files", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponse(responseCode = "200", description = "Files listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java index e2f1884e..f3ea018a 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java @@ -2,8 +2,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -33,7 +33,7 @@ public interface MemberAdminAPI { @Operation(summary = "Approve drive quota request", description = "This method approve a drive ammount quota request", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponse(responseCode = "204", description = "Approved successfuly") @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - @PostMapping("{id}/quotas/requests/approve") + @PatchMapping("{id}/quotas/requests") ResponseEntity approveQuotaRequest( @PathVariable(value = "id", required = true) String id, @RequestParam(value = "approved", defaultValue = "true") boolean approved); From 0ec76b21a6f273e340e6055e868923f1a7c52400 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Sun, 24 Aug 2025 18:48:44 -0300 Subject: [PATCH 82/89] refactor: update Spring Boot version to 3.5.5 and change deprecated specification usage in repositories --- infrastructure/build.gradle | 2 +- .../callv2/drive/infrastructure/file/FileJPAGateway.java | 2 +- .../callv2/drive/infrastructure/filter/FilterService.java | 6 +++--- .../drive/infrastructure/folder/FolderJpaGateway.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index c5ab35fc..ebd9c95d 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' id 'application' - id 'org.springframework.boot' version '3.4.6' + id 'org.springframework.boot' version '3.5.5' id 'io.spring.dependency-management' version '1.1.7' } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java index 59bda478..825302a8 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java @@ -61,7 +61,7 @@ public Page findAll(final SearchQuery searchQuery) { searchQuery.filters()); final org.springframework.data.domain.Page pageResult = this.fileRepository - .findAll(Specification.where(specification), page); + .findAll(specification, page); return new Page<>( pageResult.getNumber(), diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/filter/FilterService.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/filter/FilterService.java index c95c8fd7..5e58773f 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/filter/FilterService.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/filter/FilterService.java @@ -23,12 +23,12 @@ public Specification buildSpecification( final List filters) { if (filterMethod.equals(Filter.Operator.AND)) - return Specification.where(andSpecifications(buildSpecifications(entityClass, filters))); + return andSpecifications(buildSpecifications(entityClass, filters)); if (filterMethod.equals(Filter.Operator.OR)) - return Specification.where(orSpecifications(buildSpecifications(entityClass, filters))); + return orSpecifications(buildSpecifications(entityClass, filters)); - return Specification.where(andSpecifications(buildSpecifications(entityClass, filters))); + return andSpecifications(buildSpecifications(entityClass, filters)); } private List> buildSpecifications(Class entityClass, diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java index 59365ad8..bd2f6843 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java @@ -81,7 +81,7 @@ public Page findAll(SearchQuery searchQuery) { searchQuery.filters()); final org.springframework.data.domain.Page pageResult = this.folderRepository.findAll( - Specification.where(specification), + specification, page); return new Page<>( From fd7acb45dcf5463ca644bb3e92fbe5b49a77f201 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 25 Aug 2025 11:18:02 -0300 Subject: [PATCH 83/89] refactor: remove Guava dependency from build files and update JUnit version --- application/build.gradle | 1 - domain/build.gradle | 3 +-- gradle/libs.versions.toml | 12 ------------ infrastructure/build.gradle | 2 -- 4 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 gradle/libs.versions.toml diff --git a/application/build.gradle b/application/build.gradle index db1d4fe5..bb8ab405 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -17,7 +17,6 @@ dependencies { testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation libs.guava } java { diff --git a/domain/build.gradle b/domain/build.gradle index 2fe4511b..418db58f 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -10,11 +10,10 @@ repositories { } dependencies { - testImplementation libs.junit.jupiter + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation libs.guava } java { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 8f20ed0b..00000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,12 +0,0 @@ -# This file was generated by the Gradle 'init' task. -# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format - -[versions] -commons-math3 = "3.6.1" -guava = "33.0.0-jre" -junit-jupiter = "5.10.2" - -[libraries] -commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } -guava = { module = "com.google.guava:guava", version.ref = "guava" } -junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index ebd9c95d..7c9d9c5d 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -45,8 +45,6 @@ dependencies { // testImplementation 'com.h2database:h2:2.3.232' implementation 'com.h2database:h2:2.3.232' - implementation libs.guava - implementation 'org.springframework:spring-aop' implementation 'org.springframework.boot:spring-boot-starter-log4j2' From 5a84952ea5f47860e353b276a323d42d2ce72c46 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 25 Aug 2025 11:24:47 -0300 Subject: [PATCH 84/89] refactor: add @NonNull annotation to Jwt parameters in KeycloakAuthoritiesConverter and KeycloakJwtConverter --- .../configuration/security/KeycloakAuthoritiesConverter.java | 3 ++- .../configuration/security/KeycloakJwtConverter.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java index c77d950c..37d798c5 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java @@ -10,6 +10,7 @@ import java.util.stream.Stream; import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -25,7 +26,7 @@ public class KeycloakAuthoritiesConverter implements Converter convert(final Jwt jwt) { + public Collection convert(@NonNull final Jwt jwt) { final var realmRoles = extractRealmRoles(jwt); final var resourceRoles = extractResourceRoles(jwt); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java index 2bf950b1..b8c89789 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java @@ -3,6 +3,7 @@ import java.util.Collection; import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -18,7 +19,7 @@ public KeycloakJwtConverter() { } @Override - public AbstractAuthenticationToken convert(final Jwt jwt) { + public AbstractAuthenticationToken convert(@NonNull final Jwt jwt) { return new JwtAuthenticationToken(jwt, extractAuthorities(jwt), extractPrincipal(jwt)); } From 11641d293659db878623d2db2b5cda4f0e5fe80f Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 25 Aug 2025 11:44:54 -0300 Subject: [PATCH 85/89] refactor: update JUnit and Mockito versions across build files --- application/build.gradle | 4 ++-- domain/build.gradle | 2 +- infrastructure/build.gradle | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index bb8ab405..601801a5 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -13,8 +13,8 @@ dependencies { implementation(project(":domain")) - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' - testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' + testImplementation 'org.mockito:mockito-junit-jupiter:5.19.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/domain/build.gradle b/domain/build.gradle index 418db58f..fe967bc4 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -11,7 +11,7 @@ repositories { dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 7c9d9c5d..935c7af0 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -38,8 +38,8 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' - testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' + testImplementation 'org.mockito:mockito-junit-jupiter:5.19.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // testImplementation 'com.h2database:h2:2.3.232' From 19e5390b91f07756a4e67db727816bb3f8c77d3d Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 25 Aug 2025 11:45:44 -0300 Subject: [PATCH 86/89] chore: update dependencies to latest versions --- infrastructure/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 935c7af0..8c7a4c12 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -27,14 +27,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-amqp' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.postgresql:postgresql:42.7.5' + implementation 'org.postgresql:postgresql:42.7.7' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.11' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.19.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' From 94fc7d7194035f7ca22ef6e218538ff3d66104b2 Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 25 Aug 2025 11:45:56 -0300 Subject: [PATCH 87/89] chore: remove H2 database dependency from build.gradle --- infrastructure/build.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 8c7a4c12..d51c56ac 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -42,9 +42,6 @@ dependencies { testImplementation 'org.mockito:mockito-junit-jupiter:5.19.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - // testImplementation 'com.h2database:h2:2.3.232' - implementation 'com.h2database:h2:2.3.232' - implementation 'org.springframework:spring-aop' implementation 'org.springframework.boot:spring-boot-starter-log4j2' From ee3a6d10ea31c958369dc2095d408031d1ba996b Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 25 Aug 2025 12:01:07 -0300 Subject: [PATCH 88/89] chore: remove unused spring-aop dependency from build.gradle --- infrastructure/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index d51c56ac..222aa944 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -42,7 +42,6 @@ dependencies { testImplementation 'org.mockito:mockito-junit-jupiter:5.19.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'org.springframework:spring-aop' implementation 'org.springframework.boot:spring-boot-starter-log4j2' } From 03f223b41750759bbd8c1e0e30eda0aa357b3adc Mon Sep 17 00:00:00 2001 From: jhonatapers Date: Mon, 25 Aug 2025 12:01:52 -0300 Subject: [PATCH 89/89] chore: update JUnit and Mockito versions in build.gradle --- aop/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aop/build.gradle b/aop/build.gradle index 2bf2e09c..f6f3cb45 100644 --- a/aop/build.gradle +++ b/aop/build.gradle @@ -13,8 +13,8 @@ dependencies { implementation 'org.aspectj:aspectjrt:1.9.24' - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' - testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' + testImplementation 'org.mockito:mockito-junit-jupiter:5.19.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' }