diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8d639cf3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.github +.gradle +**/build +**/out +**/target +.idea +.vscode +**/*.iml +**/*.ipr +**/*.iws +**/.DS_Store \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..0be60ad2 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +SPRING_PROFILES_ACTIVE=env + +SERVER_PORT=8080 +FORWARD_HEADERS_STRATEGY=framework #none for no proxy, framework for Spring Cloud Gateway, native for native proxy +REQUEST_TIMEOUT=60000 + +CORS_PATTERN=/** +CORS_ALLOWED_ORIGINS=* +CORS_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS +CORS_ALLOWED_HEADERS=* +CORS_ALLOW_CREDENTIALS=true + +KEYCLOAK_HOST=http://localhost:8090 +KEYCLOAK_REALM=heroes + +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_DATABASE=postgres +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres + +DB_DDL_AUTO=none + +STORAGE_LOCATION=/srv/drive/storage/ +MAX_FILE_SIZE=2048MB +MAX_REQUEST_SIZE=100MB + +LOG_PATH=log +ROOT_LOG_LEVEL=trace +ROOT_LOG_LEVEL_CONSOLE=info +ROOT_LOG_LEVEL_FILE=info +ROOT_LOG_LEVEL_JSON=info +CALLV2_LOG_LEVEL=trace +CALLV2_LOG_LEVEL_CONSOLE=debug +CALLV2_LOG_LEVEL_FILE=debug +CALLV2_LOG_LEVEL_JSON=debug \ No newline at end of file diff --git a/.github/workflows/build-and-push-docker-image.yml b/.github/workflows/build-and-push-docker-image.yml new file mode 100644 index 00000000..8fb0ef57 --- /dev/null +++ b/.github/workflows/build-and-push-docker-image.yml @@ -0,0 +1,42 @@ +name: build-and-push-docker-image + +on: + workflow_call: + inputs: + imageName: + required: true + type: string + imageTag: + required: true + type: string + secrets: + DOCKERHUB_USERNAME: + required: true + DOCKERHUB_TOKEN: + required: true + +jobs: + docker: + runs-on: [self-hosted, Linux, X64] + steps: + + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.imageName }}:${{ inputs.imageTag }} \ No newline at end of file diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml new file mode 100644 index 00000000..7298decc --- /dev/null +++ b/.github/workflows/latest.yml @@ -0,0 +1,16 @@ +name: Release build & push to Docker Hub + +on: + push: + branches: + - main + +jobs: + release-build-push: + uses: ./.github/workflows/build-and-push-docker-image.yml + with: + imageName: drive-api + imageTag: latest + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml new file mode 100644 index 00000000..d9cf34e5 --- /dev/null +++ b/.github/workflows/nightly.yaml @@ -0,0 +1,16 @@ +name: Develop nightly build & push to Docker Hub + +on: + push: + branches: + - develop + +jobs: + nightly-build-push: + uses: ./.github/workflows/build-and-push-docker-image.yml + with: + imageName: drive-api + imageTag: nightly + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ff7b3d05 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,15 @@ +name: Release build & push to Docker Hub + +on: + release: + types: [published] + +jobs: + release-build-push: + uses: ./.github/workflows/build-and-push-docker-image.yml + with: + imageName: drive-api + imageTag: ${{ github.event.release.tag_name }} + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 17391b5c..e8fb0dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ bin *.db + + +.env + +*.log +*.log.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6aaad901 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM eclipse-temurin:21.0.7_6-jdk-alpine AS builder + +WORKDIR /usr/app + +COPY . . + +RUN ./gradlew clean bootJar --no-daemon + +FROM eclipse-temurin:21.0.7_6-jdk-alpine + +COPY --from=builder /usr/app/build/libs/application.jar /opt/app/application.jar + +ENV STORAGE_LOCATION=/srv/drive/storage + +RUN mkdir -p $STORAGE_LOCATION +RUN addgroup -S app && adduser -S app -G app +RUN chown -R app:app /srv/drive/storage/ +USER app:app + + + +EXPOSE 80 + +CMD ["sh", "-c", "java -jar /opt/app/application.jar"] \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/folder/create/CreateFolderInput.java b/application/src/main/java/com/callv2/drive/application/folder/create/CreateFolderInput.java index 2297bc3d..62946e9d 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/create/CreateFolderInput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/create/CreateFolderInput.java @@ -2,10 +2,10 @@ import java.util.UUID; -public record CreateFolderInput(String name, UUID parentFolderId) { +public record CreateFolderInput(String ownerId, String name, UUID parentFolderId) { - public static CreateFolderInput from(String name, UUID parentFolderId) { - return new CreateFolderInput(name, parentFolderId); + public static CreateFolderInput from(String ownerdId, String name, UUID parentFolderId) { + return new CreateFolderInput(ownerdId, name, parentFolderId); } } 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 0db208f5..8d67ff3c 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 @@ -6,34 +6,44 @@ 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.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.handler.Notification; public class DefaultCreateFolderUseCase extends CreateFolderUseCase { + private final MemberGateway memberGateway; private final FolderGateway folderGateway; - public DefaultCreateFolderUseCase(final FolderGateway folderGateway) { + public DefaultCreateFolderUseCase(final MemberGateway memberGateway, final FolderGateway folderGateway) { + this.memberGateway = memberGateway; this.folderGateway = folderGateway; } @Override public CreateFolderOutput execute(final CreateFolderInput input) { + final MemberID ownerId = MemberID.of(input.ownerId()); + + if (!memberGateway.existsById(ownerId)) + throw NotFoundException.with(Member.class, input.ownerId().toString()); + final Folder parentFolder = folderGateway .findById(FolderID.of(input.parentFolderId())) .orElseThrow(() -> NotFoundException.with( Folder.class, "Parent folder with id %s not found".formatted(input.parentFolderId().toString()))); - return CreateFolderOutput.from(createFolder(FolderName.of(input.name()), parentFolder)); + return CreateFolderOutput.from(createFolder(ownerId, FolderName.of(input.name()), parentFolder)); } - private Folder createFolder(final FolderName name, final Folder parentFolder) { + 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")); - final Folder folder = notification.valdiate(() -> Folder.create(name, parentFolder)); + final Folder folder = notification.valdiate(() -> 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/folder/retrieve/get/root/DefaultGetRootFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/DefaultGetRootFolderUseCase.java index de352d7e..29af302b 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/DefaultGetRootFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/DefaultGetRootFolderUseCase.java @@ -3,26 +3,39 @@ import java.util.Objects; import java.util.Optional; +import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.file.FileGateway; import com.callv2.drive.domain.folder.Folder; import com.callv2.drive.domain.folder.FolderGateway; +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; public class DefaultGetRootFolderUseCase extends GetRootFolderUseCase { + private final MemberGateway memberGateway; private final FolderGateway folderGateway; private final FileGateway fileGateway; public DefaultGetRootFolderUseCase( + final MemberGateway memberGateway, final FolderGateway folderGateway, final FileGateway fileGateway) { + this.memberGateway = Objects.requireNonNull(memberGateway); this.folderGateway = Objects.requireNonNull(folderGateway); this.fileGateway = Objects.requireNonNull(fileGateway); } @Override - public GetRootFolderOutput execute() { + public GetRootFolderOutput execute(final GetRootFolderInput input) { + + final MemberID owner = MemberID.of(input.ownerId()); + + if (!memberGateway.existsById(owner)) + throw NotFoundException.with(Member.class, owner.getValue().toString()); + final Optional root = folderGateway.findRoot(); - final Folder folder = root.isPresent() ? root.get() : folderGateway.create(Folder.createRoot()); + final Folder folder = root.isPresent() ? root.get() : folderGateway.create(Folder.createRoot(owner)); return GetRootFolderOutput.from(folder, fileGateway.findByFolder(folder.getId())); } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderInput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderInput.java new file mode 100644 index 00000000..28e9bf78 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderInput.java @@ -0,0 +1,8 @@ +package com.callv2.drive.application.folder.retrieve.get.root; +public record GetRootFolderInput(String ownerId) { + + public static GetRootFolderInput from(final String ownerId) { + return new GetRootFolderInput(ownerId); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderUseCase.java index dcefcdb4..33e76f12 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderUseCase.java @@ -1,7 +1,7 @@ package com.callv2.drive.application.folder.retrieve.get.root; -import com.callv2.drive.application.NullaryUseCase; +import com.callv2.drive.application.UseCase; -public abstract class GetRootFolderUseCase extends NullaryUseCase { +public abstract class GetRootFolderUseCase extends UseCase { } 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 2d1f7792..95b9f380 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 @@ -42,406 +42,406 @@ @ExtendWith(MockitoExtension.class) public class DefaultCreateFileUseCaseTest { - @InjectMocks - DefaultCreateFileUseCase useCase; + @InjectMocks + DefaultCreateFileUseCase useCase; - @Mock - MemberGateway memberGateway; + @Mock + MemberGateway memberGateway; - @Mock - FolderGateway folderGateway; + @Mock + FolderGateway folderGateway; - @Mock - StorageService storageService; + @Mock + StorageService storageService; - @Mock - FileGateway fileGateway; + @Mock + FileGateway fileGateway; - @Test - void givenAValidParams_whenCallsExecute_thenShouldCreateFile() { + @Test + void givenAValidParams_whenCallsExecute_thenShouldCreateFile() { - final var owner = Member.create(MemberID.of("owner")) - .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) - .approveQuotaRequest(); + final var owner = Member.create(MemberID.of("owner")) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); - final var ownerId = owner.getId(); + final var ownerId = owner.getId(); - final var folder = Folder.createRoot(); - final var expectedFolderId = folder.getId(); + final var folder = Folder.createRoot(ownerId); + final var expectedFolderId = folder.getId(); - final var expectedFileName = FileName.of("file"); - final var expectedContentType = "image/jpeg"; - final var contentBytes = "content".getBytes(); + final var expectedFileName = FileName.of("file"); + final var expectedContentType = "image/jpeg"; + final var contentBytes = "content".getBytes(); - final var expectedContent = new ByteArrayInputStream(contentBytes); - final var expectedContentSize = (long) contentBytes.length; + final var expectedContent = new ByteArrayInputStream(contentBytes); + final var expectedContentSize = (long) contentBytes.length; - when(memberGateway.findById(any())) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(any())) + .thenReturn(Optional.of(owner)); - when(fileGateway.findByFolder(any())) - .thenReturn(List.of()); + when(fileGateway.findByFolder(any())) + .thenReturn(List.of()); - when(folderGateway.findById(any())) - .thenReturn(Optional.of(folder)); + when(folderGateway.findById(any())) + .thenReturn(Optional.of(folder)); - when(storageService.store(any(), any())) - .then(returnsFirstArg()); + when(storageService.store(any(), any())) + .then(returnsFirstArg()); - when(fileGateway.create(any())) - .thenAnswer(returnsFirstArg()); + when(fileGateway.create(any())) + .thenAnswer(returnsFirstArg()); - final var input = CreateFileInput.of( - ownerId.getValue(), - expectedFolderId.getValue(), - expectedFileName.value(), - expectedContentType, - expectedContent, - expectedContentSize); + final var input = CreateFileInput.of( + ownerId.getValue(), + expectedFolderId.getValue(), + expectedFileName.value(), + expectedContentType, + expectedContent, + expectedContentSize); - final var actualOuptut = useCase.execute(input); + final var actualOuptut = useCase.execute(input); - assertNotNull(actualOuptut.id()); + assertNotNull(actualOuptut.id()); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); - verify(storageService, times(1)).store(any(), any()); - verify(storageService, times(1)).store(any(), eq(expectedContent)); - verify(storageService, times(0)).delete(any()); - verify(fileGateway, times(1)).findByFolder(any()); - verify(fileGateway, times(1)).findByFolder(eq(folder.getId())); - verify(fileGateway, times(1)).create(any()); - verify(fileGateway, times(1)).create(argThat(file -> { + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + verify(storageService, times(1)).store(any(), any()); + verify(storageService, times(1)).store(any(), eq(expectedContent)); + verify(storageService, times(0)).delete(any()); + verify(fileGateway, times(1)).findByFolder(any()); + verify(fileGateway, times(1)).findByFolder(eq(folder.getId())); + verify(fileGateway, times(1)).create(any()); + verify(fileGateway, times(1)).create(argThat(file -> { - assertEquals(actualOuptut.id().getValue(), file.getId().getValue()); - assertEquals(expectedFileName, file.getName()); - assertEquals(expectedContentType, file.getContent().type()); - assertNotNull(file.getCreatedAt()); - assertNotNull(file.getUpdatedAt()); - assertNotNull(file.getContent().location()); - assertEquals(file.getCreatedAt(), file.getUpdatedAt()); + assertEquals(actualOuptut.id().getValue(), file.getId().getValue()); + assertEquals(expectedFileName, file.getName()); + assertEquals(expectedContentType, file.getContent().type()); + assertNotNull(file.getCreatedAt()); + assertNotNull(file.getUpdatedAt()); + assertNotNull(file.getContent().location()); + assertEquals(file.getCreatedAt(), file.getUpdatedAt()); - return true; - })); + return true; + })); - } + } - @Test - void givenAnInvalidId_whenCallsExecute_thenShouldThrowNotFoundException() { + @Test + void givenAnInvalidId_whenCallsExecute_thenShouldThrowNotFoundException() { - final var owner = Member.create(MemberID.of("owner")) - .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) - .approveQuotaRequest(); + final var owner = Member.create(MemberID.of("owner")) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); - final var ownerId = owner.getId(); + final var ownerId = owner.getId(); - final var folder = Folder.createRoot(); + final var folder = Folder.createRoot(ownerId); - final var expectedFolderId = folder.getId(); + final var expectedFolderId = folder.getId(); - final var expectedFileName = FileName.of("file"); - final var expectedContentType = "image/jpeg"; - final var contentBytes = "content".getBytes(); + final var expectedFileName = FileName.of("file"); + final var expectedContentType = "image/jpeg"; + final var contentBytes = "content".getBytes(); - final var expectedContent = new ByteArrayInputStream(contentBytes); - final var expectedContentSize = (long) contentBytes.length; + 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 with id '%s' not found" + .formatted(expectedFolderId.getValue()); - when(memberGateway.findById(any())) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(any())) + .thenReturn(Optional.of(owner)); - when(folderGateway.findById(any())) - .thenReturn(Optional.empty()); + when(folderGateway.findById(any())) + .thenReturn(Optional.empty()); - final var input = CreateFileInput.of( - ownerId.getValue(), - expectedFolderId.getValue(), - expectedFileName.value(), - expectedContentType, - expectedContent, - expectedContentSize); + final var input = CreateFileInput.of( + ownerId.getValue(), + expectedFolderId.getValue(), + expectedFileName.value(), + expectedContentType, + expectedContent, + expectedContentSize); - final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); - assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedExceptionMessage, actualException.getMessage()); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); - verify(storageService, times(0)).store(any(), any()); - verify(storageService, times(0)).store(any(), eq(expectedContent)); - verify(storageService, times(0)).delete(any()); - verify(fileGateway, times(0)).findByFolder(any()); - verify(fileGateway, times(0)).create(any()); + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + verify(storageService, times(0)).store(any(), any()); + verify(storageService, times(0)).store(any(), eq(expectedContent)); + verify(storageService, times(0)).delete(any()); + verify(fileGateway, times(0)).findByFolder(any()); + verify(fileGateway, times(0)).create(any()); - } + } - @Test - void givenAValidParamsWithAlreadyExistingFileNameOnSameFolder_whenCallsExecute_thenShouldThrowValidationException() { + @Test + void givenAValidParamsWithAlreadyExistingFileNameOnSameFolder_whenCallsExecute_thenShouldThrowValidationException() { - final var owner = Member.create(MemberID.of("owner")) - .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) - .approveQuotaRequest(); + final var owner = Member.create(MemberID.of("owner")) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); - final var ownerId = owner.getId(); + final var ownerId = owner.getId(); - final var folder = Folder.createRoot(); + final var folder = Folder.createRoot(ownerId); - final var expectedFolderId = folder.getId(); + final var expectedFolderId = folder.getId(); - final var expectedFileName = FileName.of("file"); - final var expectedContentType = "image/jpeg"; - final var contentBytes = "content".getBytes(); + final var expectedFileName = FileName.of("file"); + final var expectedContentType = "image/jpeg"; + final var contentBytes = "content".getBytes(); - final var expectedContent = new ByteArrayInputStream(contentBytes); - final var expectedContentSize = (long) contentBytes.length; + final var expectedContent = new ByteArrayInputStream(contentBytes); + final var expectedContentSize = (long) contentBytes.length; - final var fileWithSameName = File.create(ownerId, folder.getId(), expectedFileName, - Content.of("location", "text", 10)); + final var fileWithSameName = File.create(ownerId, folder.getId(), expectedFileName, + Content.of("location", "text", 10)); - final var expectedExceptionMessage = "Could not create Aggregate File"; - final var expectedErrorMessage = "File with same name already exists on this folder"; + final var expectedExceptionMessage = "Could not create Aggregate File"; + final var expectedErrorMessage = "File with same name already exists on this folder"; - when(memberGateway.findById(ownerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(ownerId)) + .thenReturn(Optional.of(owner)); - when(fileGateway.findByFolder(any())) - .thenReturn(List.of(fileWithSameName)); + when(fileGateway.findByFolder(any())) + .thenReturn(List.of(fileWithSameName)); - when(folderGateway.findById(any())) - .thenReturn(Optional.of(folder)); + when(folderGateway.findById(any())) + .thenReturn(Optional.of(folder)); - when(storageService.store(any(), any())) - .then(returnsFirstArg()); + when(storageService.store(any(), any())) + .then(returnsFirstArg()); - final var input = CreateFileInput.of( - ownerId.getValue(), - expectedFolderId.getValue(), - expectedFileName.value(), - expectedContentType, - expectedContent, - expectedContentSize); + final var input = CreateFileInput.of( + ownerId.getValue(), + expectedFolderId.getValue(), + expectedFileName.value(), + expectedContentType, + expectedContent, + expectedContentSize); - final var actualException = assertThrows(ValidationException.class, () -> useCase.execute(input)); + final var actualException = assertThrows(ValidationException.class, () -> useCase.execute(input)); - assertEquals(expectedExceptionMessage, actualException.getMessage()); - assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); - verify(storageService, times(1)).store(any(), any()); - verify(storageService, times(1)).store(any(), eq(expectedContent)); - verify(storageService, times(0)).delete(any()); - verify(fileGateway, times(1)).findByFolder(any()); - verify(fileGateway, times(1)).findByFolder(eq(folder.getId())); - verify(fileGateway, times(0)).create(any()); + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + verify(storageService, times(1)).store(any(), any()); + verify(storageService, times(1)).store(any(), eq(expectedContent)); + verify(storageService, times(0)).delete(any()); + verify(fileGateway, times(1)).findByFolder(any()); + verify(fileGateway, times(1)).findByFolder(eq(folder.getId())); + verify(fileGateway, times(0)).create(any()); - } + } - @Test - void givenAValidParams_whenCallsExecuteAndFileGatewayCreateThrowsRandomException_thenShouldThrowInternalErrorException() { + @Test + void givenAValidParams_whenCallsExecuteAndFileGatewayCreateThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) - .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) - .approveQuotaRequest(); + final var owner = Member.create(MemberID.of("owner")) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); - final var ownerId = owner.getId(); + final var ownerId = owner.getId(); - final var folder = Folder.createRoot(); - final var expectedFolderId = folder.getId(); + final var folder = Folder.createRoot(ownerId); + final var expectedFolderId = folder.getId(); - final var expectedFileName = FileName.of("file"); - final var expectedContentType = "image/jpeg"; - final var contentBytes = "content".getBytes(); + final var expectedFileName = FileName.of("file"); + final var expectedContentType = "image/jpeg"; + final var contentBytes = "content".getBytes(); - final var expectedContent = new ByteArrayInputStream(contentBytes); - final var expectedContentSize = (long) contentBytes.length; + final var expectedContent = new ByteArrayInputStream(contentBytes); + final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Could not store File"; + final var expectedExceptionMessage = "Could not store File"; - when(memberGateway.findById(ownerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(ownerId)) + .thenReturn(Optional.of(owner)); - when(fileGateway.findByFolder(any())) - .thenReturn(List.of()); + when(fileGateway.findByFolder(any())) + .thenReturn(List.of()); - when(folderGateway.findById(any())) - .thenReturn(Optional.of(folder)); + when(folderGateway.findById(any())) + .thenReturn(Optional.of(folder)); - when(storageService.store(any(), any())) - .then(returnsFirstArg()); + when(storageService.store(any(), any())) + .then(returnsFirstArg()); - when(fileGateway.create(any())) - .thenThrow(new IllegalStateException("FileGateway Exception")); + when(fileGateway.create(any())) + .thenThrow(new IllegalStateException("FileGateway Exception")); - doNothing() - .when(storageService) - .delete(any()); + doNothing() + .when(storageService) + .delete(any()); - final var input = CreateFileInput.of( - ownerId.getValue(), - expectedFolderId.getValue(), - expectedFileName.value(), - expectedContentType, - expectedContent, - expectedContentSize); + final var input = CreateFileInput.of( + ownerId.getValue(), + expectedFolderId.getValue(), + expectedFileName.value(), + expectedContentType, + expectedContent, + expectedContentSize); - final var actualException = assertThrows(InternalErrorException.class, () -> useCase.execute(input)); + final var actualException = assertThrows(InternalErrorException.class, () -> useCase.execute(input)); - assertEquals(expectedExceptionMessage, actualException.getMessage()); - assertEquals("FileGateway Exception", actualException.getCause().getMessage()); + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals("FileGateway Exception", actualException.getCause().getMessage()); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); - verify(storageService, times(1)).store(any(), any()); - verify(storageService, times(1)).store(any(), eq(expectedContent)); - verify(storageService, times(1)).delete(any()); - verify(fileGateway, times(1)).findByFolder(any()); - verify(fileGateway, times(1)).findByFolder(eq(folder.getId())); - verify(fileGateway, times(1)).create(any()); - verify(fileGateway, times(1)).create(argThat(file -> { + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + verify(storageService, times(1)).store(any(), any()); + verify(storageService, times(1)).store(any(), eq(expectedContent)); + verify(storageService, times(1)).delete(any()); + verify(fileGateway, times(1)).findByFolder(any()); + verify(fileGateway, times(1)).findByFolder(eq(folder.getId())); + verify(fileGateway, times(1)).create(any()); + verify(fileGateway, times(1)).create(argThat(file -> { - assertNotNull(file.getId().getValue()); - assertEquals(expectedFileName, file.getName()); - assertEquals(expectedContentType, file.getContent().type()); - assertNotNull(file.getCreatedAt()); - assertNotNull(file.getUpdatedAt()); - assertNotNull(file.getContent().location()); - assertEquals(file.getCreatedAt(), file.getUpdatedAt()); + assertNotNull(file.getId().getValue()); + assertEquals(expectedFileName, file.getName()); + assertEquals(expectedContentType, file.getContent().type()); + assertNotNull(file.getCreatedAt()); + assertNotNull(file.getUpdatedAt()); + assertNotNull(file.getContent().location()); + assertEquals(file.getCreatedAt(), file.getUpdatedAt()); - return true; - })); + return true; + })); - } + } - @Test - void givenAValidParams_whenCallsExecuteAndFileGatewayCreateAndContentGatewayDeleteThrowsRandomException_thenShouldThrowInternalErrorException() { + @Test + void givenAValidParams_whenCallsExecuteAndFileGatewayCreateAndContentGatewayDeleteThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) - .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) - .approveQuotaRequest(); + final var owner = Member.create(MemberID.of("owner")) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); - final var ownerId = owner.getId(); + final var ownerId = owner.getId(); - final var folder = Folder.createRoot(); - final var expectedFolderId = folder.getId(); + final var folder = Folder.createRoot(ownerId); + final var expectedFolderId = folder.getId(); - final var expectedFileName = FileName.of("file"); - final var expectedContentType = "image/jpeg"; - final var contentBytes = "content".getBytes(); + final var expectedFileName = FileName.of("file"); + final var expectedContentType = "image/jpeg"; + final var contentBytes = "content".getBytes(); - final var expectedContent = new ByteArrayInputStream(contentBytes); - final var expectedContentSize = (long) contentBytes.length; + final var expectedContent = new ByteArrayInputStream(contentBytes); + final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Could not delete BinaryContent"; + final var expectedExceptionMessage = "Could not delete BinaryContent"; - when(memberGateway.findById(ownerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(ownerId)) + .thenReturn(Optional.of(owner)); - when(fileGateway.findByFolder(any())) - .thenReturn(List.of()); + when(fileGateway.findByFolder(any())) + .thenReturn(List.of()); - when(folderGateway.findById(any())) - .thenReturn(Optional.of(folder)); + when(folderGateway.findById(any())) + .thenReturn(Optional.of(folder)); - when(storageService.store(any(), any())) - .then(returnsFirstArg()); + when(storageService.store(any(), any())) + .then(returnsFirstArg()); - when(fileGateway.create(any())) - .thenThrow(new IllegalStateException("FileGateway Exception")); + when(fileGateway.create(any())) + .thenThrow(new IllegalStateException("FileGateway Exception")); - doThrow(new IllegalStateException("ContentGateway Exception")) - .when(storageService) - .delete(any()); + doThrow(new IllegalStateException("ContentGateway Exception")) + .when(storageService) + .delete(any()); - final var input = CreateFileInput.of( - ownerId.getValue(), - expectedFolderId.getValue(), - expectedFileName.value(), - expectedContentType, - expectedContent, - expectedContentSize); + final var input = CreateFileInput.of( + ownerId.getValue(), + expectedFolderId.getValue(), + expectedFileName.value(), + expectedContentType, + expectedContent, + expectedContentSize); - final var actualException = assertThrows(InternalErrorException.class, () -> useCase.execute(input)); + final var actualException = assertThrows(InternalErrorException.class, () -> useCase.execute(input)); - assertEquals(expectedExceptionMessage, actualException.getMessage()); - assertEquals("ContentGateway Exception", actualException.getCause().getMessage()); + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals("ContentGateway Exception", actualException.getCause().getMessage()); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); - verify(storageService, times(1)).store(any(), any()); - verify(storageService, times(1)).store(any(), eq(expectedContent)); - verify(storageService, times(1)).delete(any()); - verify(fileGateway, times(1)).findByFolder(any()); - verify(fileGateway, times(1)).findByFolder(eq(folder.getId())); - verify(fileGateway, times(1)).create(any()); - verify(fileGateway, times(1)).create(argThat(file -> { + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + verify(storageService, times(1)).store(any(), any()); + verify(storageService, times(1)).store(any(), eq(expectedContent)); + verify(storageService, times(1)).delete(any()); + verify(fileGateway, times(1)).findByFolder(any()); + verify(fileGateway, times(1)).findByFolder(eq(folder.getId())); + verify(fileGateway, times(1)).create(any()); + verify(fileGateway, times(1)).create(argThat(file -> { - assertNotNull(file.getId().getValue()); - assertEquals(expectedFileName, file.getName()); - assertEquals(expectedContentType, file.getContent().type()); - assertNotNull(file.getCreatedAt()); - assertNotNull(file.getUpdatedAt()); - assertNotNull(file.getContent().location()); - assertEquals(file.getCreatedAt(), file.getUpdatedAt()); + assertNotNull(file.getId().getValue()); + assertEquals(expectedFileName, file.getName()); + assertEquals(expectedContentType, file.getContent().type()); + assertNotNull(file.getCreatedAt()); + assertNotNull(file.getUpdatedAt()); + assertNotNull(file.getContent().location()); + assertEquals(file.getCreatedAt(), file.getUpdatedAt()); - return true; - })); + return true; + })); - } + } - @Test - void givenAValidParams_whenCallsExecuteAndContentGatewayStoreThrowsRandomException_thenShouldThrowInternalErrorException() { + @Test + void givenAValidParams_whenCallsExecuteAndContentGatewayStoreThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) - .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) - .approveQuotaRequest(); + final var owner = Member.create(MemberID.of("owner")) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); - final var ownerId = owner.getId(); + final var ownerId = owner.getId(); - final var folder = Folder.createRoot(); - final var expectedFolderId = folder.getId(); + final var folder = Folder.createRoot(ownerId); + final var expectedFolderId = folder.getId(); - final var expectedFileName = FileName.of("file"); - final var expectedContentType = "image/jpeg"; - final var contentBytes = "content".getBytes(); + final var expectedFileName = FileName.of("file"); + final var expectedContentType = "image/jpeg"; + final var contentBytes = "content".getBytes(); - final var expectedContent = new ByteArrayInputStream(contentBytes); - final var expectedContentSize = (long) contentBytes.length; + final var expectedContent = new ByteArrayInputStream(contentBytes); + final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Could not store BinaryContent"; + final var expectedExceptionMessage = "Could not store BinaryContent"; - when(memberGateway.findById(ownerId)) - .thenReturn(Optional.of(owner)); + when(memberGateway.findById(ownerId)) + .thenReturn(Optional.of(owner)); - when(folderGateway.findById(any())) - .thenReturn(Optional.of(folder)); + when(folderGateway.findById(any())) + .thenReturn(Optional.of(folder)); - when(storageService.store(any(), any())) - .thenThrow(new IllegalStateException("ContentGateway Exception")); + when(storageService.store(any(), any())) + .thenThrow(new IllegalStateException("ContentGateway Exception")); - final var input = CreateFileInput.of( - ownerId.getValue(), - expectedFolderId.getValue(), - expectedFileName.value(), - expectedContentType, - expectedContent, - expectedContentSize); + final var input = CreateFileInput.of( + ownerId.getValue(), + expectedFolderId.getValue(), + expectedFileName.value(), + expectedContentType, + expectedContent, + expectedContentSize); - final var actualException = assertThrows(InternalErrorException.class, () -> useCase.execute(input)); + final var actualException = assertThrows(InternalErrorException.class, () -> useCase.execute(input)); - assertEquals(expectedExceptionMessage, actualException.getMessage()); - assertEquals("ContentGateway Exception", actualException.getCause().getMessage()); + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals("ContentGateway Exception", actualException.getCause().getMessage()); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); - verify(storageService, times(1)).store(any(), any()); - verify(storageService, times(1)).store(any(), eq(expectedContent)); - verify(storageService, times(0)).delete(any()); - verify(fileGateway, times(0)).findByFolder(any()); - verify(fileGateway, times(0)).create(any()); + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + verify(storageService, times(1)).store(any(), any()); + verify(storageService, times(1)).store(any(), eq(expectedContent)); + verify(storageService, times(0)).delete(any()); + verify(fileGateway, times(0)).findByFolder(any()); + verify(fileGateway, times(0)).create(any()); - } + } } 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 de3a5171..6f93cbd9 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 @@ -40,7 +40,7 @@ void givenAValidId_whenCallsExecute_thenShouldReturnFile() { final var ownerId = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot(); + final var expectedFolder = Folder.createRoot(ownerId); final var expectedName = "file.jpeg"; @@ -50,7 +50,8 @@ void givenAValidId_whenCallsExecute_thenShouldReturnFile() { final var expectedContent = Content.of(expectedContentLocation, expectedContentType, expectedContentSize); - final var expectedFile = File.create(ownerId, expectedFolder.getId(), FileName.of(expectedName), expectedContent); + final var expectedFile = File.create(ownerId, expectedFolder.getId(), FileName.of(expectedName), + expectedContent); final var expectedId = expectedFile.getId(); 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 802d719f..e079f46e 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 @@ -17,6 +17,7 @@ import com.callv2.drive.domain.folder.Folder; import com.callv2.drive.domain.folder.FolderGateway; import com.callv2.drive.domain.folder.FolderName; +import com.callv2.drive.domain.member.MemberID; @ExtendWith(MockitoExtension.class) public class DefaultMoveFolderUseCaseTest { @@ -30,10 +31,12 @@ public class DefaultMoveFolderUseCaseTest { @Test void givenVAlidInput_whenCallsExecute_thenMoveFolder() { - final var expectedRootFolder = Folder.createRoot(); + final var ownerId = MemberID.of("owner"); - final var expectedFolderToMove = Folder.create(FolderName.of("folder1"), expectedRootFolder); - final var expectedFolderTarget = Folder.create(FolderName.of("folder2"), expectedRootFolder); + final var expectedRootFolder = Folder.createRoot(ownerId); + + final var expectedFolderToMove = Folder.create(ownerId, FolderName.of("folder1"), expectedRootFolder); + final var expectedFolderTarget = Folder.create(ownerId, FolderName.of("folder2"), expectedRootFolder); when(folderGateway.findById(expectedFolderToMove.getId())) .thenReturn(Optional.of(expectedFolderToMove)); 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 768418d7..68e524fc 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 @@ -23,6 +23,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.member.MemberID; @ExtendWith(MockitoExtension.class) public class DefaultGetFolderUseCaseTest { @@ -39,8 +40,14 @@ public class DefaultGetFolderUseCaseTest { @Test void givenAValidFolderId_whenCallsExecute_thenShouldReturnFolder() { + final var ownerId = MemberID.of("owner"); + final var expectedFolderName = "folder"; - final var expectedFolder = Folder.create(FolderName.of(expectedFolderName), Folder.createRoot()); + 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(); 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 4d52b76f..39a42fda 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 @@ -6,6 +6,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.ValidationHandler; import com.callv2.drive.domain.validation.handler.Notification; @@ -14,6 +15,8 @@ public class Folder extends AggregateRoot { private boolean rootFolder; + private MemberID owner; + private FolderName name; private FolderID parentFolder; private Set subFolders; @@ -24,6 +27,7 @@ public class Folder extends AggregateRoot { private Folder( final FolderID id, + final MemberID owner, final FolderName name, final FolderID parentFolder, final Set subFolders, @@ -32,6 +36,8 @@ private Folder( final Instant deletedAt, final boolean rootFolder) { super(id); + + this.owner = owner; this.name = name; this.parentFolder = parentFolder; this.subFolders = subFolders == null ? new HashSet<>() : new HashSet<>(subFolders); @@ -45,6 +51,7 @@ private Folder( public static Folder with( final FolderID id, + final MemberID owner, final FolderName name, final FolderID parentFolder, final Set subFolders, @@ -52,14 +59,15 @@ public static Folder with( final Instant updatedAt, final Instant deletedAt, final boolean rootFolder) { - return new Folder(id, name, parentFolder, subFolders, createdAt, updatedAt, deletedAt, rootFolder); + return new Folder(id, owner, name, parentFolder, subFolders, createdAt, updatedAt, deletedAt, rootFolder); } - public static Folder createRoot() { + public static Folder createRoot(final MemberID owner) { Instant now = Instant.now(); return Folder.with( FolderID.unique(), + owner, FolderName.of("Root"), null, new HashSet<>(), @@ -70,6 +78,7 @@ public static Folder createRoot() { } public static Folder create( + final MemberID owner, final FolderName name, final Folder parentFolder) { @@ -77,6 +86,7 @@ public static Folder create( final var folder = Folder.with( FolderID.unique(), + owner, name, parentFolder.getId(), new HashSet<>(), @@ -162,6 +172,10 @@ public boolean isRootFolder() { return rootFolder; } + public MemberID getOwner() { + return owner; + } + public FolderID getParentFolder() { return parentFolder; } 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 ac18ec44..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 @@ -15,4 +15,6 @@ public interface MemberGateway { Page findAllQuotaRequests(final SearchQuery searchQuery); + Boolean existsById(MemberID id); + } diff --git a/domain/src/test/java/com/callv2/drive/domain/file/FileTest.java b/domain/src/test/java/com/callv2/drive/domain/file/FileTest.java index 7c89ee2e..ae1f9414 100644 --- a/domain/src/test/java/com/callv2/drive/domain/file/FileTest.java +++ b/domain/src/test/java/com/callv2/drive/domain/file/FileTest.java @@ -17,8 +17,10 @@ public class FileTest { @Test void givenAValidParams_whenCallsCreate_thenShouldCreateFile() { + final var ownerId = MemberID.of("owner"); + final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(ownerId).getId(); final var expectedName = "file"; @@ -44,7 +46,7 @@ void givenAValidParams_whenCallsCreate_thenShouldCreateFile() { void givenEmptyName_whenCallsCreate_thenShouldThrowsValidationException() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final var expectedName = ""; @@ -70,7 +72,7 @@ void givenEmptyName_whenCallsCreate_thenShouldThrowsValidationException() { void givenNullName_whenCallsCreate_thenShouldThrowsValidationException() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final String expectedName = null; @@ -100,7 +102,7 @@ void givenNullName_whenCallsCreate_thenShouldThrowsValidationException() { void givenNameWithMoreThan64Chars_whenCallsCreate_thenShouldThrowsValidationException() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final var expectedName = """ filefilefilefilefilefilefilefilefilefilefilefilefilefilefilefilefilefile @@ -133,7 +135,7 @@ void givenNameWithMoreThan64Chars_whenCallsCreate_thenShouldThrowsValidationExce void givenReservedName_whenCallsCreate_thenShouldThrowsValidationException() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final var expectedName = "nul"; @@ -164,7 +166,7 @@ void givenReservedName_whenCallsCreate_thenShouldThrowsValidationException() { void givenMultipleInvalidParams_whenCallsCreate_thenShouldThrowsValidationExceptionWithMultipleErrors() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final String expectedName = "nul"; @@ -196,7 +198,7 @@ void givenMultipleInvalidParams_whenCallsCreate_thenShouldThrowsValidationExcept void givenAValidParams_whenCallsUpdate_thenShouldCreateFile() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final var expectedName = FileName.of("File"); @@ -227,7 +229,7 @@ void givenAValidParams_whenCallsUpdate_thenShouldCreateFile() { void givenEmptyName_whenCallsUpdate_thenShouldThrowsValidationException() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final var expectedName = ""; @@ -254,7 +256,7 @@ void givenEmptyName_whenCallsUpdate_thenShouldThrowsValidationException() { void givenNullName_whenCallsUpdate_thenShouldThrowsValidationException() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final String expectedName = null; @@ -282,7 +284,7 @@ void givenNullName_whenCallsUpdate_thenShouldThrowsValidationException() { void givenNameWithMoreThan64Chars_whenCallsUpdate_thenShouldThrowsValidationException() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final var expectedName = """ filefilefilefilefilefilefilefilefilefilefilefilefilefilefilefilefilefile @@ -312,7 +314,7 @@ void givenNameWithMoreThan64Chars_whenCallsUpdate_thenShouldThrowsValidationExce void givenReservedName_whenCallsUpdate_thenShouldThrowsValidationException() { final var expectedOwner = MemberID.of("owner"); - final var expectedFolder = Folder.createRoot().getId(); + final var expectedFolder = Folder.createRoot(expectedOwner).getId(); final var expectedName = "nul"; diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 4db40c4e..4305c30a 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -1,12 +1,16 @@ plugins { id 'java' id 'application' - id 'org.springframework.boot' version '3.4.1' + id 'org.springframework.boot' version '3.4.5' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.callv2' -version = '1.0-SNAPSHOT' + +bootJar { + archiveFileName = 'application.jar' + destinationDirectory = file("${rootProject.buildDir}/libs") +} repositories { mavenCentral() @@ -39,6 +43,16 @@ dependencies { implementation 'com.h2database:h2:2.3.232' implementation libs.guava + + implementation 'org.springframework:spring-aop' + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + +} + +configurations { + all { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + } } java { 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 new file mode 100644 index 00000000..b7aa2c67 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/AspectExecutorChain.java @@ -0,0 +1,76 @@ +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 new file mode 100644 index 00000000..2f0e954d --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/MethodInvocationAspectExecutorChain.java @@ -0,0 +1,23 @@ +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 new file mode 100644 index 00000000..8f4a953b --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/PostInvocationAspectExecutorChain.java @@ -0,0 +1,25 @@ +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 new file mode 100644 index 00000000..1dad486c --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/Proceeder.java @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..1ba24ab1 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java @@ -0,0 +1,27 @@ +package com.callv2.drive.infrastructure.aop.aspects.context; + +import java.time.Instant; + +import javax.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 new file mode 100644 index 00000000..bdeae37f --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractPostInvocationContext.java @@ -0,0 +1,44 @@ +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 new file mode 100644 index 00000000..151fb732 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java @@ -0,0 +1,19 @@ +package com.callv2.drive.infrastructure.aop.aspects.context; + +import java.time.Instant; + +import javax.annotation.Nonnull; + +import org.aopalliance.intercept.MethodInvocation; + +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 new file mode 100644 index 00000000..41a206b8 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java @@ -0,0 +1,20 @@ +package com.callv2.drive.infrastructure.aop.aspects.context; + +import java.time.Instant; + +import javax.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 new file mode 100644 index 00000000..b5e5d4ea --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimpleMethodInvocationContext.java @@ -0,0 +1,64 @@ +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 new file mode 100644 index 00000000..30b55dfc --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimplePostInvocationContext.java @@ -0,0 +1,104 @@ +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 new file mode 100644 index 00000000..8519ed3e --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ArgsLogExecutor.java @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000..9b7cf5da --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/AspectExecutor.java @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..8a96e709 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/IdentifiableAspectExecutor.java @@ -0,0 +1,11 @@ +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/Log4jExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/Log4jExecutor.java new file mode 100644 index 00000000..16195a1f --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/Log4jExecutor.java @@ -0,0 +1,44 @@ +package com.callv2.drive.infrastructure.aop.aspects.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 { + + private final Logger logger; + private final Level logLevel; + + public Log4jExecutor(final Level logLevel, Class clazz) { + super(); + this.logLevel = logLevel; + this.logger = LogManager.getLogger(clazz); + } + + public void log(final String message) { + logger.log(logLevel, createMessage(message)); + } + + public void log(final Object message) { + logger.log(logLevel, createMessage(message)); + } + + public void log(final String message, final Object... params) { + logger.log(logLevel, createMessage(message, params)); + } + + private Message createMessage(final String message) { + return logger.getMessageFactory().newMessage(message); + } + + private Message createMessage(final Object message) { + return logger.getMessageFactory().newMessage(message); + } + + private Message createMessage(final String message, final Object... params) { + return logger.getMessageFactory().newMessage(message, params); + } + +} 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 new file mode 100644 index 00000000..4c8ec16b --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/MethodSignatureLogExecutor.java @@ -0,0 +1,18 @@ +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 new file mode 100644 index 00000000..48675064 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/PostTelemetryLogExecutor.java @@ -0,0 +1,22 @@ +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 new file mode 100644 index 00000000..6bef77d9 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java @@ -0,0 +1,31 @@ +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); + } + + @SuppressWarnings("null") + @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 new file mode 100644 index 00000000..4ae41e75 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java @@ -0,0 +1,50 @@ +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; + +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; + +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/api/FileAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java index 401394b1..bd0cd9ab 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 @@ -44,7 +44,7 @@ public interface FileAPI { @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) }) ResponseEntity create( - @PathVariable UUID folderId, + @PathVariable(required = true, name = "folderId") UUID folderId, @RequestPart("file") MultipartFile file); @GetMapping(value = "{id}", 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 f2c4e21a..1a13f32e 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 @@ -11,6 +11,7 @@ import com.callv2.drive.application.folder.move.MoveFolderInput; import com.callv2.drive.application.folder.move.MoveFolderUseCase; import com.callv2.drive.application.folder.retrieve.get.GetFolderUseCase; +import com.callv2.drive.application.folder.retrieve.get.root.GetRootFolderInput; import com.callv2.drive.application.folder.retrieve.get.root.GetRootFolderUseCase; import com.callv2.drive.application.folder.retrieve.list.ListFoldersUseCase; import com.callv2.drive.domain.pagination.Filter; @@ -27,6 +28,7 @@ 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; @RestController public class FolderController implements FolderAPI { @@ -52,12 +54,15 @@ public FolderController( @Override public ResponseEntity getRoot() { - return ResponseEntity.ok(FolderPresenter.present(getRootFolderUseCase.execute())); + return ResponseEntity.ok(FolderPresenter.present( + getRootFolderUseCase.execute(GetRootFolderInput.from(SecurityContext.getAuthenticatedUser())))); } @Override public ResponseEntity create(final CreateFolderRequest request) { - final var response = FolderPresenter.present(createFolderUseCase.execute(FolderAdapter.adapt(request))); + final String ownerId = SecurityContext.getAuthenticatedUser(); + final var response = FolderPresenter + .present(createFolderUseCase.execute(FolderAdapter.adapt(request, ownerId))); return ResponseEntity .created(URI.create("/folders/" + response.id())) 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 new file mode 100644 index 00000000..e76fca6c --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/aspect/AspectConfig.java @@ -0,0 +1,65 @@ +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 new file mode 100644 index 00000000..c930113e --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/aspect/PointcutAdvisorProperties.java @@ -0,0 +1,48 @@ +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; + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/cors/CorsConfigurationProperties.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/cors/CorsConfigurationProperties.java new file mode 100644 index 00000000..f369194d --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/cors/CorsConfigurationProperties.java @@ -0,0 +1,53 @@ +package com.callv2.drive.infrastructure.configuration.properties.cors; + +import java.util.List; + +public class CorsConfigurationProperties { + + private String pattern; + private List allowedOrigins; + private List allowedMethods; + private List allowedHeaders; + private boolean allowCredentials; + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public List getAllowedOrigins() { + return allowedOrigins; + } + + public void setAllowedOrigins(List allowedOriginsPatterns) { + this.allowedOrigins = allowedOriginsPatterns; + } + + public List getAllowedMethods() { + return allowedMethods; + } + + public void setAllowedMethods(List allowedMethods) { + this.allowedMethods = allowedMethods; + } + + public List getAllowedHeaders() { + return allowedHeaders; + } + + public void setAllowedHeaders(List allowedHeaders) { + this.allowedHeaders = allowedHeaders; + } + + public boolean isAllowCredentials() { + return allowCredentials; + } + + public void setAllowCredentials(boolean allowCredentials) { + this.allowCredentials = allowCredentials; + } + +} 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 new file mode 100644 index 00000000..c77d950c --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java @@ -0,0 +1,67 @@ +package com.callv2.drive.infrastructure.configuration.security; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap; + +public class KeycloakAuthoritiesConverter implements Converter> { + + private static final String REALM_ACCESS = "realm_access"; + private static final String ROLES = "roles"; + private static final String RESOURCE_ACCESS = "resource_access"; + private static final String SEPARATOR = "_"; + private static final String ROLE_PREFIX = "ROLE_"; + + @Override + public Collection convert(final Jwt jwt) { + final var realmRoles = extractRealmRoles(jwt); + final var resourceRoles = extractResourceRoles(jwt); + + return Stream.concat(realmRoles, resourceRoles) + .map(role -> new SimpleGrantedAuthority(ROLE_PREFIX + role.toUpperCase())) + .collect(Collectors.toSet()); + } + + private Stream extractResourceRoles(final Jwt jwt) { + + final Function, Stream> mapResource = resource -> { + final var key = resource.getKey(); + @SuppressWarnings("rawtypes") + final LinkedTreeMap value = (LinkedTreeMap) resource.getValue(); + @SuppressWarnings("unchecked") + final var roles = (Collection) value.get(ROLES); + return roles.stream().map(role -> key.concat(SEPARATOR).concat(role)); + }; + + final Function>, Collection> mapResources = resources -> resources + .stream() + .flatMap(mapResource) + .toList(); + + return Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS)) + .map(resources -> resources.entrySet()) + .map(mapResources) + .orElse(Collections.emptyList()) + .stream(); + } + + @SuppressWarnings("unchecked") + private Stream extractRealmRoles(final Jwt jwt) { + return Optional.ofNullable(jwt.getClaimAsMap(REALM_ACCESS)) + .map(resource -> (Collection) resource.get(ROLES)) + .orElse(Collections.emptyList()) + .stream(); + } +} 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 new file mode 100644 index 00000000..2bf950b1 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java @@ -0,0 +1,32 @@ +package com.callv2.drive.infrastructure.configuration.security; + +import java.util.Collection; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +public class KeycloakJwtConverter implements Converter { + + private final KeycloakAuthoritiesConverter authoritiesConverter; + + public KeycloakJwtConverter() { + this.authoritiesConverter = new KeycloakAuthoritiesConverter(); + } + + @Override + public AbstractAuthenticationToken convert(final Jwt jwt) { + return new JwtAuthenticationToken(jwt, extractAuthorities(jwt), extractPrincipal(jwt)); + } + + private String extractPrincipal(final Jwt jwt) { + return jwt.getClaimAsString(JwtClaimNames.SUB); + } + + private Collection extractAuthorities(final Jwt jwt) { + return this.authoritiesConverter.convert(jwt); + } +} 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 b8b94fdb..6995bff3 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 @@ -1,31 +1,21 @@ package com.callv2.drive.infrastructure.configuration.security; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap; +import com.callv2.drive.infrastructure.configuration.properties.cors.CorsConfigurationProperties; @Configuration @EnableWebSecurity @@ -35,13 +25,18 @@ public class SecurityConfig { private static final String ROLE_ADMIN = "ADMINISTRADOR"; @Bean - public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { + SecurityFilterChain securityFilterChain( + final HttpSecurity http, + final CorsConfigurationSource corsConfigurationSource) throws Exception { return http - .csrf(csrf -> { - csrf.disable(); - }) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authorize -> { authorize + + .requestMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() + .requestMatchers("admin/**") .hasAnyRole(ROLE_ADMIN) @@ -57,82 +52,43 @@ public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws E }) .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer .jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtConverter()))) - .sessionManagement(session -> { - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS); - }) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .build(); } - static class KeycloakJwtConverter implements Converter { - - private final KeycloakAuthoritiesConverter authoritiesConverter; + @Bean + CorsConfigurationSource corsConfigurationSource(final CorsConfigurationProperties corsProperties) { - public KeycloakJwtConverter() { - this.authoritiesConverter = new KeycloakAuthoritiesConverter(); - } + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - @Override - public AbstractAuthenticationToken convert(final Jwt jwt) { - return new JwtAuthenticationToken(jwt, extractAuthorities(jwt), extractPrincipal(jwt)); - } + source.registerCorsConfiguration( + corsProperties.getPattern(), + corsConfiguration( + corsProperties.getAllowedOrigins(), + corsProperties.getAllowedMethods(), + corsProperties.getAllowedHeaders(), + corsProperties.isAllowCredentials())); - private String extractPrincipal(final Jwt jwt) { - return jwt.getClaimAsString(JwtClaimNames.SUB); - } + return source; + } - private Collection extractAuthorities(final Jwt jwt) { - return this.authoritiesConverter.convert(jwt); - } + static CorsConfiguration corsConfiguration( + final List allowedOriginsPatterns, + final List allowedMethods, + final List allowedHeaders, + final boolean allowCredentials) { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(allowedOriginsPatterns); + configuration.setAllowedMethods(allowedMethods); + configuration.setAllowedHeaders(allowedHeaders); + configuration.setAllowCredentials(allowCredentials); + return configuration; } - static class KeycloakAuthoritiesConverter implements Converter> { - - private static final String REALM_ACCESS = "realm_access"; - private static final String ROLES = "roles"; - private static final String RESOURCE_ACCESS = "resource_access"; - private static final String SEPARATOR = "_"; - private static final String ROLE_PREFIX = "ROLE_"; - - @Override - public Collection convert(final Jwt jwt) { - final var realmRoles = extractRealmRoles(jwt); - final var resourceRoles = extractResourceRoles(jwt); - - return Stream.concat(realmRoles, resourceRoles) - .map(role -> new SimpleGrantedAuthority(ROLE_PREFIX + role.toUpperCase())) - .collect(Collectors.toSet()); - } - - private Stream extractResourceRoles(final Jwt jwt) { - - final Function, Stream> mapResource = resource -> { - final var key = resource.getKey(); - @SuppressWarnings("rawtypes") - final LinkedTreeMap value = (LinkedTreeMap) resource.getValue(); - @SuppressWarnings("unchecked") - final var roles = (Collection) value.get(ROLES); - return roles.stream().map(role -> key.concat(SEPARATOR).concat(role)); - }; - - final Function>, Collection> mapResources = resources -> resources - .stream() - .flatMap(mapResource) - .toList(); - - return Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS)) - .map(resources -> resources.entrySet()) - .map(mapResources) - .orElse(Collections.emptyList()) - .stream(); - } - - @SuppressWarnings("unchecked") - private Stream extractRealmRoles(final Jwt jwt) { - return Optional.ofNullable(jwt.getClaimAsMap(REALM_ACCESS)) - .map(resource -> (Collection) resource.get(ROLES)) - .orElse(Collections.emptyList()) - .stream(); - } + @Bean + @ConfigurationProperties("security.cors") + CorsConfigurationProperties corsConfigurationProperties() { + return new CorsConfigurationProperties(); } } 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..960229c1 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 @@ -15,28 +15,32 @@ import com.callv2.drive.application.folder.retrieve.list.ListFoldersUseCase; import com.callv2.drive.domain.file.FileGateway; import com.callv2.drive.domain.folder.FolderGateway; +import com.callv2.drive.domain.member.MemberGateway; @Configuration public class FolderUseCaseConfig { private final FolderGateway folderGateway; private final FileGateway fileGateway; + private final MemberGateway memberGateway; public FolderUseCaseConfig( final FolderGateway folderGateway, - final FileGateway fileGateway) { + final FileGateway fileGateway, + final MemberGateway memberGateway) { this.folderGateway = folderGateway; this.fileGateway = fileGateway; + this.memberGateway = memberGateway; } @Bean GetRootFolderUseCase getRootFolderUseCase() { - return new DefaultGetRootFolderUseCase(folderGateway, fileGateway); + return new DefaultGetRootFolderUseCase(memberGateway, folderGateway, fileGateway); } @Bean CreateFolderUseCase createFolderUseCase() { - return new DefaultCreateFolderUseCase(folderGateway); + return new DefaultCreateFolderUseCase(memberGateway, folderGateway); } @Bean diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/adapter/FolderAdapter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/adapter/FolderAdapter.java index 0e0322f8..8093410f 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/adapter/FolderAdapter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/adapter/FolderAdapter.java @@ -8,8 +8,8 @@ public interface FolderAdapter { - static CreateFolderInput adapt(CreateFolderRequest request) { - return CreateFolderInput.from(request.name(), request.parentFolderId()); + static CreateFolderInput adapt(CreateFolderRequest request, String ownerId) { + return CreateFolderInput.from(ownerId, request.name(), request.parentFolderId()); } static GetFolderInput adapt(UUID id) { 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 285f12b7..53eb4d6d 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 @@ -10,6 +10,7 @@ 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; @@ -31,6 +32,9 @@ public class FolderJpaEntity { @Column(name = "name", nullable = false) private String name; + @Column(name = "owner_id", nullable = false) + private String ownerId; + @Column(name = "parent_folder_id") private UUID parentFolderId; @@ -50,6 +54,7 @@ private FolderJpaEntity( final UUID id, final Boolean rootFolder, final String name, + final String ownerId, final UUID parentFolderId, final Instant createdAt, final Instant updatedAt, @@ -57,6 +62,7 @@ private FolderJpaEntity( this.id = id; this.rootFolder = rootFolder; this.name = name; + this.ownerId = ownerId; this.parentFolderId = parentFolderId; this.createdAt = createdAt; this.updatedAt = updatedAt; @@ -69,13 +75,13 @@ public FolderJpaEntity() { } public static FolderJpaEntity fromDomain(final Folder folder) { - final UUID parentFolderId = folder.getParentFolder() == null ? null : folder.getParentFolder().getValue(); final var entity = new FolderJpaEntity( folder.getId().getValue(), folder.isRootFolder(), folder.getName().value(), + folder.getOwner().getValue(), parentFolderId, folder.getCreatedAt(), folder.getUpdatedAt(), @@ -90,6 +96,7 @@ public static FolderJpaEntity fromDomain(final Folder folder) { public Folder toDomain() { return Folder.with( FolderID.of(id), + MemberID.of(ownerId), FolderName.of(name), FolderID.of(parentFolderId), subFolders.stream() @@ -99,7 +106,6 @@ public Folder toDomain() { updatedAt, deletedAt, rootFolder); - } public void addSubFolder(final SubFolder anId) { @@ -130,6 +136,14 @@ public void setName(String name) { this.name = name; } + public String getOwnerId() { + return ownerId; + } + + public void setOwnerId(String ownerId) { + this.ownerId = ownerId; + } + public UUID getParentFolderId() { return parentFolderId; } @@ -169,5 +183,4 @@ public Instant getDeletedAt() { public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } - -} +} \ No newline at end of file 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 2ce444ff..12a0000f 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 @@ -60,4 +60,9 @@ public Page findAllQuotaRequests(SearchQuery searchQuery) { pageResult.toList()); } + @Override + public Boolean existsById(MemberID id) { + return this.memberJpaRepository.existsById(id.getValue()); + } + } diff --git a/infrastructure/src/main/resources/application-env.yml b/infrastructure/src/main/resources/application-env.yml new file mode 100644 index 00000000..1ad58a86 --- /dev/null +++ b/infrastructure/src/main/resources/application-env.yml @@ -0,0 +1,33 @@ +server: + port: ${SERVER_PORT} + forward-headers-strategy: ${FORWARD_HEADERS_STRATEGY:none} + +security: + cors: + pattern: ${CORS_PATTERN} + allowed-origins: ${CORS_ALLOWED_ORIGINS} + allowed-methods: ${CORS_ALLOWED_METHODS} + allowed-headers: ${CORS_ALLOWED_HEADERS} + allow-credentials: ${CORS_ALLOW_CREDENTIALS} + +keycloak: + realm: ${KEYCLOAK_REALM} + host: ${KEYCLOAK_HOST} + +postgres: + host: ${POSTGRES_HOST} + port: ${POSTGRES_PORT} + database: ${POSTGRES_DATABASE} + username: ${POSTGRES_USERNAME} + password: ${POSTGRES_PASSWORD} + +db: + ddl-auto: ${DB_DDL_AUTO} + +reuqest-timeout: ${REQUEST_TIMEOUT} + +storage: + max-file-size: ${MAX_FILE_SIZE} + max-request-size: ${MAX_REQUEST_SIZE} + file-system: + location: ${STORAGE_LOCATION} diff --git a/infrastructure/src/main/resources/application-local.yml b/infrastructure/src/main/resources/application-local.yml deleted file mode 100644 index 144ca9e0..00000000 --- a/infrastructure/src/main/resources/application-local.yml +++ /dev/null @@ -1,18 +0,0 @@ -keycloak: - realm: callv2 - host: http://localhost:8090 - client: - user-api: - client-id: user-api - client-secret: local-client-secret - -postgres: - host: localhost - port: 5433 - database: drive - username: postgres - password: postgres - -storage: - file-system: - location: /home/jhonatapers/callv2/storage \ No newline at end of file diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index ef1e152b..e933fa4f 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -1,5 +1,6 @@ server: - port: 8080 + port: 80 + forward-headers-strategy: none servlet: context-path: /api compression: @@ -8,14 +9,16 @@ server: min-response-size: 1024 spring: + profiles: + active: ${SPRING_PROFILES_ACTIVE:prd} mvc: async: - request-timeout: 600000 + request-timeout: ${reuqest-timeout:30000} servlet: multipart: enabled: true - max-file-size: 10240MB - max-request-size: 10240MB + max-file-size: ${storage.max-file-size} + max-request-size: ${storage.max-request-size} security: oauth2: resourceserver: @@ -29,5 +32,5 @@ spring: url: jdbc:postgresql://${postgres.host}:${postgres.port}/${postgres.database} jpa: hibernate: - ddl-auto: none - show-sql: true \ No newline at end of file + ddl-auto: ${db.ddl-auto} + show-sql: false diff --git a/infrastructure/src/main/resources/log4j2-spring.xml b/infrastructure/src/main/resources/log4j2-spring.xml new file mode 100644 index 00000000..eecde3e7 --- /dev/null +++ b/infrastructure/src/main/resources/log4j2-spring.xml @@ -0,0 +1,89 @@ + + + + ${env:ROOT_LOG_LEVEL:-info} + ${env:ROOT_LOG_LEVEL_CONSOLE:-info} + ${env:ROOT_LOG_LEVEL_FILE:-info} + ${env:ROOT_LOG_LEVEL_JSON:-info} + + ${env:CALLV2_LOG_LEVEL:-info} + ${env:CALLV2_LOG_LEVEL_CONSOLE:-info} + ${env:CALLV2_LOG_LEVEL_FILE:-info} + ${env:CALLV2_LOG_LEVEL_JSON:-info} + + ${env:LOG_PATH:-log} + %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %X{traceId} %-5level %logger{36}.%M(%F:%L) - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file