diff --git a/.env.example b/.env.example index 55bf672f..c6608b31 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,13 @@ POSTGRES_PASSWORD=postgres DB_DDL_AUTO=none +RABBITMQ_VIRTUAL_HOST=/ +RABBITMQ_AUTO_CREATE=true +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_USERNAME=admin +RABBITMQ_PASSWORD=admin + STORAGE_LOCATION=/srv/drive/storage/ MAX_FILE_SIZE=2048MB MAX_REQUEST_SIZE=100MB diff --git a/.gitignore b/.gitignore index 2868c3c3..c4615161 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ bin .vscode +.idea/ *.db @@ -17,4 +18,7 @@ bin .env *.log -*log.json \ No newline at end of file +*log.json + +*.log.gz +*log.json.gz \ No newline at end of file diff --git a/aop/build.gradle b/aop/build.gradle new file mode 100644 index 00000000..f6f3cb45 --- /dev/null +++ b/aop/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java-library' +} + +group = 'com.callv2' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + + implementation 'org.aspectj:aspectjrt:1.9.24' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' + testImplementation 'org.mockito:mockito-junit-jupiter:5.19.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java new file mode 100644 index 00000000..5f16443c --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/AbstractInvocationContext.java @@ -0,0 +1,89 @@ +package com.callv2.aop.context; + +import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.reflect.SourceLocation; +import org.aspectj.runtime.internal.AroundClosure; + +public abstract class AbstractInvocationContext implements InvocationContext { + + protected final ProceedingJoinPoint joinPoint; + protected final AtomicBoolean proceeded; + private final Instant contextedAt; + + protected AbstractInvocationContext(final ProceedingJoinPoint joinPoint) { + this.joinPoint = joinPoint; + this.proceeded = new AtomicBoolean(false); + this.contextedAt = Instant.now(); + } + + protected AbstractInvocationContext(final PreInvocationContext preInvocationContext) { + this.joinPoint = preInvocationContext; + this.proceeded = new AtomicBoolean(preInvocationContext.proceeded()); + this.contextedAt = preInvocationContext.getContextedAt(); + } + + @Override + public void set$AroundClosure(AroundClosure arc) { + this.joinPoint.set$AroundClosure(arc); + } + + @Override + public String toShortString() { + return this.joinPoint.toShortString(); + } + + @Override + public String toLongString() { + return this.joinPoint.toLongString(); + } + + @Override + public Object getThis() { + return this.joinPoint.getThis(); + } + + @Override + public Object getTarget() { + return this.joinPoint.getTarget(); + } + + @Override + public Object[] getArgs() { + return this.joinPoint.getArgs(); + } + + @Override + public Signature getSignature() { + return this.joinPoint.getSignature(); + } + + @Override + public SourceLocation getSourceLocation() { + return this.joinPoint.getSourceLocation(); + } + + @Override + public String getKind() { + return this.joinPoint.getKind(); + } + + @Override + public StaticPart getStaticPart() { + return this.joinPoint.getStaticPart(); + } + + @Override + public Instant getContextedAt() { + return this.contextedAt; + } + + @Override + public boolean proceeded() { + return this.proceeded.get(); + } + +} diff --git a/aop/src/main/java/com/callv2/aop/context/InvocationContext.java b/aop/src/main/java/com/callv2/aop/context/InvocationContext.java new file mode 100644 index 00000000..84e485fd --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/InvocationContext.java @@ -0,0 +1,13 @@ +package com.callv2.aop.context; + +import java.time.Instant; + +import org.aspectj.lang.ProceedingJoinPoint; + +public interface InvocationContext extends ProceedingJoinPoint { + + Instant getContextedAt(); + + boolean proceeded(); + +} diff --git a/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java new file mode 100644 index 00000000..f2a9f5b2 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/PostInvocationContext.java @@ -0,0 +1,15 @@ +package com.callv2.aop.context; + +import java.time.Instant; + +public interface PostInvocationContext extends InvocationContext { + + Instant getProceededAt(); + + Object getResult(); + + Throwable getThrowable(); + + boolean wasSuccessful(); + +} \ No newline at end of file diff --git a/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java new file mode 100644 index 00000000..b2989833 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/PreInvocationContext.java @@ -0,0 +1,7 @@ +package com.callv2.aop.context; + +public interface PreInvocationContext extends InvocationContext { + + PostInvocationContext proceedWithContext(); + +} diff --git a/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java new file mode 100644 index 00000000..d150913b --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/SimplePostInvocationContext.java @@ -0,0 +1,98 @@ +package com.callv2.aop.context; + +import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class SimplePostInvocationContext extends AbstractInvocationContext implements PostInvocationContext { + + private final Object result; + private final Throwable throwable; + private final Instant proceededAt; + private final AtomicBoolean successful; + + private SimplePostInvocationContext( + final PreInvocationContext preInvocationContext, + final Object result, + final Throwable throwable, + final Instant proceededAt, + final boolean successful) { + super(preInvocationContext); + this.result = result; + this.throwable = throwable; + this.proceededAt = proceededAt; + this.successful = new AtomicBoolean(successful); + } + + protected static SimplePostInvocationContext from(final PreInvocationContext preInvocationContext) { + + Object result; + Throwable throwable; + boolean successful; + final Instant proceededAt; + + try { + result = preInvocationContext.proceed(); + throwable = null; + successful = true; + } catch (Throwable e) { + result = null; + throwable = e; + successful = false; + } finally { + proceededAt = Instant.now(); + } + + return new SimplePostInvocationContext( + preInvocationContext, + result, + throwable, + proceededAt, + successful); + + } + + @Override + public Object proceed() throws Throwable { + if (this.proceeded.get()) { + if (successful.get()) + return result; + else + throw throwable; + } + + return this.joinPoint.proceed(); + } + + @Override + public Object proceed(Object[] args) throws Throwable { + if (this.proceeded.get()) { + if (successful.get()) + return result; + else + throw throwable; + } + + return this.joinPoint.proceed(args); + } + + @Override + public Instant getProceededAt() { + return this.proceededAt; + } + + @Override + public Object getResult() { + return this.result; + } + + @Override + public Throwable getThrowable() { + return this.throwable; + } + + @Override + public boolean wasSuccessful() { + return this.successful.get(); + } + +} diff --git a/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java b/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java new file mode 100644 index 00000000..bbda8f5a --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/context/SimplePreInvocationContext.java @@ -0,0 +1,34 @@ +package com.callv2.aop.context; + +import org.aspectj.lang.ProceedingJoinPoint; + +public final class SimplePreInvocationContext extends AbstractInvocationContext implements PreInvocationContext { + + private SimplePreInvocationContext(final ProceedingJoinPoint joinPoint) { + super(joinPoint); + } + + public static SimplePreInvocationContext of(final ProceedingJoinPoint joinPoint) { + return new SimplePreInvocationContext(joinPoint); + } + + @Override + public Object proceed() throws Throwable { + if (proceeded.getAndSet(true)) + throw new IllegalStateException("Method already proceeded"); + return joinPoint.proceed(); + } + + @Override + public Object proceed(Object[] args) throws Throwable { + if (proceeded.getAndSet(true)) + throw new IllegalStateException("Method already proceeded"); + return joinPoint.proceed(args); + } + + @Override + public PostInvocationContext proceedWithContext() { + return SimplePostInvocationContext.from(this); + } + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/Executor.java b/aop/src/main/java/com/callv2/aop/executor/Executor.java new file mode 100644 index 00000000..f735e76e --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/Executor.java @@ -0,0 +1,10 @@ +package com.callv2.aop.executor; + +import org.aspectj.lang.ProceedingJoinPoint; + +@FunctionalInterface +public interface Executor { + + void execute(J joinPoint); + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/PostExecutor.java b/aop/src/main/java/com/callv2/aop/executor/PostExecutor.java new file mode 100644 index 00000000..37642b05 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/PostExecutor.java @@ -0,0 +1,7 @@ +package com.callv2.aop.executor; + +import com.callv2.aop.context.PostInvocationContext; + +public interface PostExecutor extends Executor { + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/PreExecutor.java b/aop/src/main/java/com/callv2/aop/executor/PreExecutor.java new file mode 100644 index 00000000..1fcf32b0 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/PreExecutor.java @@ -0,0 +1,7 @@ +package com.callv2.aop.executor; + +import com.callv2.aop.context.PreInvocationContext; + +public interface PreExecutor extends Executor { + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/ExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/ExecutorChain.java new file mode 100644 index 00000000..44397761 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/ExecutorChain.java @@ -0,0 +1,32 @@ +package com.callv2.aop.executor.chain; + +import org.aspectj.lang.ProceedingJoinPoint; + +import com.callv2.aop.executor.Executor; + +public abstract class ExecutorChain> { + + private ExecutorChain next; + private final E executor; + + protected ExecutorChain(final E executor) { + this.executor = executor; + } + + public ExecutorChain setNext(final ExecutorChain executorChain) { + return this.next = executorChain; + } + + public final O execute(final J joinpoint) throws Throwable { + + this.executor.execute(joinpoint); + + if (next != null) + return next.execute(joinpoint); + + return resolve(joinpoint); + } + + protected abstract O resolve(J joinPoint) throws Throwable; + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java new file mode 100644 index 00000000..0dedd463 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/PostInvocationExecutorChain.java @@ -0,0 +1,21 @@ +package com.callv2.aop.executor.chain; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.executor.PostExecutor; + +public final class PostInvocationExecutorChain + extends ExecutorChain { + + public PostInvocationExecutorChain(final PostExecutor executor) { + super(executor); + } + + @Override + protected Object resolve(final PostInvocationContext joinPoint) throws Throwable { + if (joinPoint.wasSuccessful()) + return joinPoint.getResult(); + else + throw joinPoint.getThrowable(); + } + +} \ No newline at end of file diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java b/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java new file mode 100644 index 00000000..1f6e0b8f --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/PreInvocationExecutorChain.java @@ -0,0 +1,19 @@ +package com.callv2.aop.executor.chain; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.PreExecutor; + +public final class PreInvocationExecutorChain + extends ExecutorChain { + + public PreInvocationExecutorChain(final PreExecutor executor) { + super(executor); + } + + @Override + protected PostInvocationContext resolve(final PreInvocationContext joinPoint) throws Throwable { + return joinPoint.proceedWithContext(); + } + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/ExecutorChainHandler.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/ExecutorChainHandler.java new file mode 100644 index 00000000..3d214c6b --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/ExecutorChainHandler.java @@ -0,0 +1,10 @@ +package com.callv2.aop.executor.chain.handler; + +import org.aspectj.lang.ProceedingJoinPoint; + +@FunctionalInterface +public interface ExecutorChainHandler { + + Object handle(final ProceedingJoinPoint joinPoint) throws Throwable; + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java new file mode 100644 index 00000000..1d7033d8 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandler.java @@ -0,0 +1,53 @@ +package com.callv2.aop.executor.chain.handler; + +import org.aspectj.lang.ProceedingJoinPoint; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.context.SimplePreInvocationContext; +import com.callv2.aop.executor.chain.PostInvocationExecutorChain; +import com.callv2.aop.executor.chain.PreInvocationExecutorChain; + +public final class SimpleExecutorChainHandler implements ExecutorChainHandler { + + private final PreInvocationExecutorChain preInvocationExecutorChain; + private final PostInvocationExecutorChain postInvocationExecutorChain; + private final PostInvocationExecutorChain errorInvocationExecutorChain; + + private final PreInvocationExecutorChain noOpPreInvocationExecutorChain = new PreInvocationExecutorChain(j -> { + }); + + private final PostInvocationExecutorChain noOpPostInvocationExecutorChain = new PostInvocationExecutorChain(j -> { + }); + + public SimpleExecutorChainHandler( + final PreInvocationExecutorChain preInvocationExecutorChain, + final PostInvocationExecutorChain postInvocationExecutorChain, + final PostInvocationExecutorChain errorInvocationExecutorChain) { + this.preInvocationExecutorChain = preInvocationExecutorChain == null ? noOpPreInvocationExecutorChain + : preInvocationExecutorChain; + this.postInvocationExecutorChain = postInvocationExecutorChain == null ? noOpPostInvocationExecutorChain + : postInvocationExecutorChain; + this.errorInvocationExecutorChain = errorInvocationExecutorChain == null ? noOpPostInvocationExecutorChain + : errorInvocationExecutorChain; + } + + @Override + public Object handle(final ProceedingJoinPoint joinPoint) throws Throwable { + + final SimplePreInvocationContext preContext = SimplePreInvocationContext.of(joinPoint); + final PostInvocationContext postContext = preInvocationExecutorChain.execute(preContext); + + if (postContext.wasSuccessful()) + return postInvocationExecutorChain.execute(postContext); + else + errorInvocationExecutorChain.execute(postContext); + + final Throwable throwable = postContext.getThrowable(); + if (throwable != null) + throw throwable; + + throw new IllegalStateException("Invocation failed but no throwable was provided."); + + } + +} diff --git a/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java new file mode 100644 index 00000000..6204e8c2 --- /dev/null +++ b/aop/src/main/java/com/callv2/aop/executor/chain/handler/SimpleExecutorChainHandlerBuilder.java @@ -0,0 +1,83 @@ +package com.callv2.aop.executor.chain.handler; + +import java.util.Queue; + +import org.aspectj.lang.ProceedingJoinPoint; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.Executor; +import com.callv2.aop.executor.PostExecutor; +import com.callv2.aop.executor.PreExecutor; +import com.callv2.aop.executor.chain.ExecutorChain; +import com.callv2.aop.executor.chain.PostInvocationExecutorChain; +import com.callv2.aop.executor.chain.PreInvocationExecutorChain; + +public final class SimpleExecutorChainHandlerBuilder { + + private final Queue> preInvocationExecutorChainQueue; + private final Queue> postInvocationExecutorChainQueue; + private final Queue> errorInvocationExecutorChainQueue; + + public SimpleExecutorChainHandlerBuilder() { + this.preInvocationExecutorChainQueue = new java.util.LinkedList<>(); + this.postInvocationExecutorChainQueue = new java.util.LinkedList<>(); + this.errorInvocationExecutorChainQueue = new java.util.LinkedList<>(); + } + + public static SimpleExecutorChainHandlerBuilder create() { + return new SimpleExecutorChainHandlerBuilder(); + } + + public SimpleExecutorChainHandlerBuilder preExecutor(final PreExecutor executor) { + this.preInvocationExecutorChainQueue.add(new PreInvocationExecutorChain(executor)); + return this; + } + + public SimpleExecutorChainHandlerBuilder postExecutor(final PostExecutor executor) { + this.postInvocationExecutorChainQueue.add(new PostInvocationExecutorChain(executor)); + return this; + } + + public SimpleExecutorChainHandlerBuilder errorExecutor(final PostExecutor executor) { + this.errorInvocationExecutorChainQueue.add(new PostInvocationExecutorChain(executor)); + return this; + } + + private static PreInvocationExecutorChain buildPreInvocationChain( + final Queue> chains) { + + return (PreInvocationExecutorChain) buildInvocationChain(chains); + } + + private PostInvocationExecutorChain buildPostInvocationChain( + Queue> chains) { + + return (PostInvocationExecutorChain) buildInvocationChain(chains); + } + + private static > ExecutorChain buildInvocationChain( + final Queue> chains) { + if (chains.isEmpty()) + return null; + + final var firstChain = chains.poll(); + var chain = firstChain; + + do { + final var next = chains.poll(); + chain = chain.setNext(next); + } while (!chains.isEmpty()); + + return firstChain; + } + + public SimpleExecutorChainHandler build() { + + return new SimpleExecutorChainHandler( + buildPreInvocationChain(preInvocationExecutorChainQueue), + buildPostInvocationChain(postInvocationExecutorChainQueue), + buildPostInvocationChain(errorInvocationExecutorChainQueue)); + } + +} diff --git a/application/build.gradle b/application/build.gradle index db1d4fe5..601801a5 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -13,11 +13,10 @@ dependencies { implementation(project(":domain")) - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' - testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' + testImplementation 'org.mockito:mockito-junit-jupiter:5.19.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation libs.guava } java { diff --git a/application/src/main/java/com/callv2/drive/application/UnitUseCase.java b/application/src/main/java/com/callv2/drive/application/UnitUseCase.java index 5d91159a..023e00c1 100644 --- a/application/src/main/java/com/callv2/drive/application/UnitUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/UnitUseCase.java @@ -2,6 +2,6 @@ public abstract class UnitUseCase { - public abstract void execute(IN anIn); + public abstract void execute(IN input); } diff --git a/application/src/main/java/com/callv2/drive/application/UseCase.java b/application/src/main/java/com/callv2/drive/application/UseCase.java index cb1cbd2b..13f5a9c3 100644 --- a/application/src/main/java/com/callv2/drive/application/UseCase.java +++ b/application/src/main/java/com/callv2/drive/application/UseCase.java @@ -2,6 +2,6 @@ public abstract class UseCase { - public abstract OUT execute(IN anIn); + public abstract OUT execute(IN input); } diff --git a/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java index bfa504aa..26fc101a 100644 --- a/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCase.java @@ -20,7 +20,7 @@ import com.callv2.drive.domain.member.MemberGateway; import com.callv2.drive.domain.member.MemberID; import com.callv2.drive.domain.storage.StorageService; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; public class DefaultCreateFileUseCase extends CreateFileUseCase { @@ -58,8 +58,7 @@ public CreateFileOutput execute(final CreateFileInput input) { .sum(); if (actualUsedQuota + input.size() > owner.getQuota().sizeInBytes()) - throw QuotaExceededException.with("Quota exceeded", - Error.with("You have exceeded your actual quota of " + owner.getQuota().sizeInBytes() + " bytes")); + throw QuotaExceededException.with(owner.getQuota()); final FolderID folderId = folderGateway .findById(FolderID.of(input.folderId())) @@ -76,7 +75,7 @@ public CreateFileOutput execute(final CreateFileInput input) { final List filesOnSameFolder = fileGateway.findByFolder(folderId); if (filesOnSameFolder.stream().map(File::getName).anyMatch(fileName::equals)) throw ValidationException.with("Could not create Aggregate File", - Error.with("File with same name already exists on this folder")); + ValidationError.with("File with same name already exists on this folder")); final String randomContentName = UUID.randomUUID().toString(); final String contentLocation = storeContentFile(randomContentName, input.content()); @@ -85,7 +84,7 @@ public CreateFileOutput execute(final CreateFileInput input) { final Content content = Content.of(contentLocation, contentType, contentSize); - final File file = notification.valdiate(() -> File.create(ownerId, folderId, fileName, content)); + final File file = notification.validate(() -> File.create(ownerId, folderId, fileName, content)); if (notification.hasError()) throw ValidationException.with("Could not create Aggregate File", notification); diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java new file mode 100644 index 00000000..a5279c75 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCase.java @@ -0,0 +1,57 @@ +package com.callv2.drive.application.file.delete; + +import java.util.Objects; + +import com.callv2.drive.domain.exception.InternalErrorException; +import com.callv2.drive.domain.exception.NotAllowedException; +import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.domain.file.File; +import com.callv2.drive.domain.file.FileGateway; +import com.callv2.drive.domain.file.FileID; +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.storage.StorageService; + +public class DefaultDeleteFileUseCase extends DeleteFileUseCase { + + private final MemberGateway memberGateway; + private final FileGateway fileGateway; + private final StorageService storageService; + + public DefaultDeleteFileUseCase( + final MemberGateway memberGateway, + final FileGateway fileGateway, + final StorageService storageService) { + this.memberGateway = Objects.requireNonNull(memberGateway); + this.fileGateway = Objects.requireNonNull(fileGateway); + this.storageService = Objects.requireNonNull(storageService); + } + + @Override + public void execute(final DeleteFileInput input) { + final MemberID ownerId = MemberID.of(input.deleterId()); + final FileID fileId = FileID.of(input.fileId()); + + final Member member = memberGateway.findById(ownerId) + .orElseThrow(() -> NotFoundException.with(Member.class, input.deleterId())); + + if (!member.hasSystemAccess()) + throw NotAllowedException.with("Member does not have permission to delete files."); + + final File file = fileGateway.findById(fileId) + .orElseThrow(() -> NotFoundException.with(File.class, input.fileId().toString())); + + fileGateway.deleteById(file.getId()); // TODO maybe needs transactional + deleteContentFile(file.getContent().location()); + } + + private void deleteContentFile(final String contentLocation) { + try { + storageService.delete(contentLocation); + } catch (Exception e) { + throw InternalErrorException.with("Could not delete BinaryContent", e); + } + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java new file mode 100644 index 00000000..d14afe18 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileInput.java @@ -0,0 +1,11 @@ +package com.callv2.drive.application.file.delete; + +import java.util.UUID; + +public record DeleteFileInput(String deleterId, UUID fileId) { + + public static DeleteFileInput of(final String deleterId, final UUID fileId) { + return new DeleteFileInput(deleterId, fileId); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileUseCase.java b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileUseCase.java new file mode 100644 index 00000000..64658391 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/file/delete/DeleteFileUseCase.java @@ -0,0 +1,6 @@ +package com.callv2.drive.application.file.delete; + +import com.callv2.drive.application.UnitUseCase; + +public abstract class DeleteFileUseCase extends UnitUseCase { +} diff --git a/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java index 8d67ff3c..72c8cb2d 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/create/DefaultCreateFolderUseCase.java @@ -1,5 +1,7 @@ package com.callv2.drive.application.folder.create; +import java.util.Set; + import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.folder.Folder; @@ -9,7 +11,7 @@ import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; import com.callv2.drive.domain.member.MemberID; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; public class DefaultCreateFolderUseCase extends CreateFolderUseCase { @@ -39,11 +41,15 @@ public CreateFolderOutput execute(final CreateFolderInput input) { } private Folder createFolder(final MemberID ownerId, FolderName name, final Folder parentFolder) { + final Notification notification = Notification.create(); - if (parentFolder.getSubFolders().stream().anyMatch(subFolder -> subFolder.name().equals(name))) - notification.append(Error.with("Folder with the same name already exists")); - final Folder folder = notification.valdiate(() -> Folder.create(ownerId, name, parentFolder)); + final Set subFolders = folderGateway.findByParentFolderId(parentFolder.getId()); + + if (subFolders.stream().anyMatch(subFolder -> subFolder.getName().equals(name))) + notification.append(ValidationError.with("Folder with the same name already exists")); + + final Folder folder = notification.validate(() -> Folder.create(ownerId, name, parentFolder)); if (notification.hasError()) throw ValidationException.with("Could not create Aggregate Folder", notification); diff --git a/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java index b8a17411..88d126eb 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCase.java @@ -1,14 +1,15 @@ package com.callv2.drive.application.folder.move; -import java.util.List; import java.util.Objects; +import java.util.Set; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.folder.Folder; import com.callv2.drive.domain.folder.FolderGateway; import com.callv2.drive.domain.folder.FolderID; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; +import com.callv2.drive.domain.validation.handler.Notification; public class DefaultMoveFolderUseCase extends MoveFolderUseCase { @@ -27,16 +28,14 @@ public void execute(MoveFolderInput input) { final Folder folder = findFolder(folderId); final Folder newParentFolder = findFolder(newParentFolderId); - if (!canMove(folder, newParentFolder)) - throw ValidationException.with("Invalid move operation", Error.with("Cannot move folder")); + final Notification notification = Notification.create(); + validateMove(folder, newParentFolder, notification); + if (notification.hasError()) + throw ValidationException.with("Invalid move operation", notification); - final Folder oldParentFolder = findFolder(folder.getParentFolder()); - oldParentFolder.removeSubFolder(folder); - - newParentFolder.addSubFolder(folder); folder.changeParentFolder(newParentFolder); - folderGateway.updateAll(List.of(folder, newParentFolder, oldParentFolder)); + folderGateway.update(folder); } private Folder findFolder(FolderID id) { @@ -45,29 +44,35 @@ private Folder findFolder(FolderID id) { .orElseThrow(() -> NotFoundException.with(Folder.class, id.getValue().toString())); } - private boolean canMove(Folder folder, Folder newParentFolder) { + private void validateMove(final Folder folder, final Folder newParentFolder, final Notification notification) { + + final Set newParentFolderSubFolders = this.folderGateway.findByParentFolderId(newParentFolder.getId()); - if (newParentFolder.getSubFolders().stream().anyMatch(sf -> sf.name().equals(folder.getName()))) - return false; + if (newParentFolderSubFolders.stream().anyMatch(sf -> sf.getName().equals(folder.getName()))) + notification.append( + ValidationError.with("A folder with the same name already exists in the target parent folder.")); if (folder.equals(newParentFolder)) - return false; + notification.append(ValidationError.with("Cannot move a folder into itself.")); if (folder.isRootFolder()) - return false; + notification.append(ValidationError.with("Cannot move the root folder.")); if (newParentFolder.isRootFolder()) - return true; + return; Folder actualParent = newParentFolder; while (!actualParent.isRootFolder()) { - if (folder.equals(actualParent)) - return false; + if (folder.equals(actualParent)) { + notification.append( + ValidationError.with("Cannot move a folder into one of its subfolders.")); + break; + } actualParent = findFolder(actualParent.getParentFolder()); } - return true; + return; } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCase.java index 18668196..222f6f40 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCase.java @@ -28,7 +28,10 @@ public GetFolderOutput execute(GetFolderInput input) { .orElseThrow(() -> NotFoundException.with(Folder.class, input.id().toString())); return GetFolderOutput - .from(folder, fileGateway.findByFolder(folder.getId())); + .from( + folder, + folderGateway.findByParentFolderId(folder.getId()), + fileGateway.findByFolder(folder.getId())); } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java index 4b041eb2..9c0a9c55 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/GetFolderOutput.java @@ -2,37 +2,43 @@ import java.time.Instant; import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import com.callv2.drive.domain.folder.Folder; public record GetFolderOutput( UUID id, String name, + Boolean isRootFolder, UUID parentFolder, - List subFolders, - List files, + Set subFolders, + Set files, + String ownerId, Instant createdAt, Instant updatedAt, Instant deletedAt) { - public static GetFolderOutput from(final Folder folder, List files) { - - final var subFolders = folder.getSubFolders() != null - ? folder.getSubFolders() - .stream() - .map(GetFolderOutput.SubFolder::from) - .toList() - : null; - - final var filesOutput = files != null ? files.stream().map(GetFolderOutput.File::from).toList() : null; + public static GetFolderOutput from( + final Folder folder, + final Set subFolders, + final List files) { return new GetFolderOutput( folder.getId().getValue(), folder.getName().value(), + folder.isRootFolder(), folder.getParentFolder().getValue(), - subFolders, - filesOutput, + subFolders + .stream() + .map(GetFolderOutput.SubFolder::from) + .collect(Collectors.toSet()), + files + .stream() + .map(GetFolderOutput.File::from) + .collect(Collectors.toSet()), + folder.getOwner().getValue(), folder.getCreatedAt(), folder.getUpdatedAt(), folder.getDeletedAt()); @@ -40,8 +46,8 @@ public static GetFolderOutput from(final Folder folder, List root = folderGateway.findRoot(); final Folder folder = root.isPresent() ? root.get() : folderGateway.create(Folder.createRoot(owner)); - return GetRootFolderOutput.from(folder, fileGateway.findByFolder(folder.getId())); + + return GetRootFolderOutput.from( + folder, + this.folderGateway.findByParentFolderId(folder.getId()), + fileGateway.findByFolder(folder.getId())); } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java index b29c31f3..f34fb9f7 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/get/root/GetRootFolderOutput.java @@ -2,31 +2,41 @@ import java.time.Instant; import java.util.List; +import java.util.Set; import java.util.UUID; import com.callv2.drive.domain.folder.Folder; public record GetRootFolderOutput( UUID id, + String name, List subFolders, List files, - Instant createdAt) { + String ownerId, + Instant createdAt, + Instant updatedAt) { - public static GetRootFolderOutput from(final Folder folder, List files) { + public static GetRootFolderOutput from( + final Folder folder, + final Set subFolders, + final List files) { final var filesOutput = files != null ? files.stream().map(GetRootFolderOutput.File::from).toList() : null; return new GetRootFolderOutput( folder.getId().getValue(), - folder.getSubFolders().stream().map(GetRootFolderOutput.SubFolder::from).toList(), + folder.getName().value(), + subFolders.stream().map(GetRootFolderOutput.SubFolder::from).toList(), filesOutput, - folder.getCreatedAt()); + folder.getOwner().getValue(), + folder.getCreatedAt(), + folder.getUpdatedAt()); } public static record SubFolder(UUID id, String name) { - public static GetRootFolderOutput.SubFolder from(com.callv2.drive.domain.folder.SubFolder subFolder) { - return new SubFolder(subFolder.id().getValue(), subFolder.name().value()); + public static GetRootFolderOutput.SubFolder from(final Folder subFolder) { + return new SubFolder(subFolder.getId().getValue(), subFolder.getName().value()); } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/retrieve/list/FolderListOutput.java b/application/src/main/java/com/callv2/drive/application/folder/retrieve/list/FolderListOutput.java index f2c091fc..44bc5b62 100644 --- a/application/src/main/java/com/callv2/drive/application/folder/retrieve/list/FolderListOutput.java +++ b/application/src/main/java/com/callv2/drive/application/folder/retrieve/list/FolderListOutput.java @@ -1,6 +1,5 @@ package com.callv2.drive.application.folder.retrieve.list; -import java.util.List; import java.util.UUID; import com.callv2.drive.domain.folder.Folder; @@ -8,21 +7,19 @@ public record FolderListOutput( UUID id, String name, - UUID parentFolder, - List subFolders) { + UUID parentFolder) { - public static FolderListOutput from(Folder folder) { + public static FolderListOutput from(final Folder folder) { return new FolderListOutput( folder.getId().getValue(), folder.getName().value(), - folder.getParentFolder().getValue(), - folder.getSubFolders().stream().map(SubFolder::from).toList()); + folder.getParentFolder().getValue()); } public static record SubFolder(UUID id, String name) { - public static FolderListOutput.SubFolder from(com.callv2.drive.domain.folder.SubFolder subFolder) { - return new FolderListOutput.SubFolder(subFolder.id().getValue(), subFolder.name().value()); + public static FolderListOutput.SubFolder from(final Folder subFolder) { + return new FolderListOutput.SubFolder(subFolder.getId().getValue(), subFolder.getName().value()); } } diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java new file mode 100644 index 00000000..d3267a16 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/DefaultUpdateFolderNameUseCase.java @@ -0,0 +1,44 @@ +package com.callv2.drive.application.folder.update.name; + +import java.util.Set; + +import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.domain.exception.ValidationException; +import com.callv2.drive.domain.folder.Folder; +import com.callv2.drive.domain.folder.FolderGateway; +import com.callv2.drive.domain.folder.FolderID; +import com.callv2.drive.domain.folder.FolderName; +import com.callv2.drive.domain.validation.ValidationError; +import com.callv2.drive.domain.validation.handler.Notification; + +public class DefaultUpdateFolderNameUseCase extends UpdateFolderNameUseCase { + + private final FolderGateway folderGateway; + + public DefaultUpdateFolderNameUseCase(final FolderGateway folderGateway) { + this.folderGateway = folderGateway; + } + + @Override + public void execute(final UpdateFolderNameInput input) { + + final Folder folder = this.folderGateway + .findById(FolderID.of(input.folderId())) + .orElseThrow(() -> NotFoundException.with(Folder.class, input.folderId().toString())); + + final FolderName folderName = FolderName.of(input.name()); + + final Notification notification = Notification.create(); + + final Set subFolders = folderGateway.findByParentFolderId(folder.getParentFolder()); + + if (subFolders.stream().anyMatch(subFolder -> subFolder.getName().equals(folderName))) + notification.append(ValidationError.with("Folder with the same name already exists")); + + if (notification.hasError()) + throw ValidationException.with("Could not update folder name", notification); + + this.folderGateway.update(folder.changeName(folderName)); + } + +} \ No newline at end of file diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java new file mode 100644 index 00000000..f590bf3b --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameInput.java @@ -0,0 +1,13 @@ +package com.callv2.drive.application.folder.update.name; + +import java.util.UUID; + +public record UpdateFolderNameInput( + UUID folderId, + String name) { + + public static UpdateFolderNameInput of(final UUID folderId, final String name) { + return new UpdateFolderNameInput(folderId, name); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameUseCase.java b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameUseCase.java new file mode 100644 index 00000000..5b588846 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/folder/update/name/UpdateFolderNameUseCase.java @@ -0,0 +1,7 @@ +package com.callv2.drive.application.folder.update.name; + +import com.callv2.drive.application.UnitUseCase; + +public abstract class UpdateFolderNameUseCase extends UnitUseCase { + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java index bd67e719..53f56acb 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/create/DefaultCreateRequestQuotaUseCase.java @@ -2,6 +2,7 @@ import java.util.Objects; +import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; @@ -24,10 +25,10 @@ public void execute(final CreateRequestQuotaInput input) { final Member member = memberGateway .findById(memberId) - .orElse(memberGateway.create(Member.create(memberId))); + .orElseThrow(() -> NotFoundException.with(Member.class, input.memberId())); final Notification notification = Notification.create(); - notification.valdiate(() -> member.requestQuota(Quota.of(input.ammount(), input.unit()))); + notification.validate(() -> member.requestQuota(Quota.of(input.ammount(), input.unit()))); if (notification.hasError()) throw ValidationException.with("Request Quota Error", notification); diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/DefaultListRequestQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/DefaultListRequestQuotaUseCase.java index e58969a1..10fe5b5f 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/DefaultListRequestQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/DefaultListRequestQuotaUseCase.java @@ -15,10 +15,10 @@ public DefaultListRequestQuotaUseCase(final MemberGateway memberGateway) { } @Override - public Page execute(final SearchQuery searchQuery) { + public Page execute(final SearchQuery searchQuery) { return memberGateway .findAllQuotaRequests(searchQuery) - .map(RequestQuotaListOutput::from); + .map(ListRequestQuotaOutput::from); } } diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java new file mode 100644 index 00000000..304fd469 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaOutput.java @@ -0,0 +1,26 @@ +package com.callv2.drive.application.member.quota.request.list; + +import java.time.Instant; + +import com.callv2.drive.domain.member.QuotaRequestPreview; +import com.callv2.drive.domain.member.QuotaUnit; + +public record ListRequestQuotaOutput( + String memberId, + String memberUsername, + String memberNickname, + long quotaAmount, + QuotaUnit quotaUnit, + Instant quotaRequestedAt) { + + public static ListRequestQuotaOutput from(final QuotaRequestPreview quotaRequestPreview) { + return new ListRequestQuotaOutput( + quotaRequestPreview.memberId(), + quotaRequestPreview.memberUsername(), + quotaRequestPreview.memberNickname(), + quotaRequestPreview.quotaAmount(), + quotaRequestPreview.quotaUnit(), + quotaRequestPreview.quotaRequestedAt()); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaUseCase.java index b945ad76..5eff4bb0 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/ListRequestQuotaUseCase.java @@ -4,6 +4,6 @@ import com.callv2.drive.domain.pagination.Page; import com.callv2.drive.domain.pagination.SearchQuery; -public abstract class ListRequestQuotaUseCase extends UseCase> { +public abstract class ListRequestQuotaUseCase extends UseCase> { } diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/RequestQuotaListOutput.java b/application/src/main/java/com/callv2/drive/application/member/quota/request/list/RequestQuotaListOutput.java deleted file mode 100644 index ed551e1d..00000000 --- a/application/src/main/java/com/callv2/drive/application/member/quota/request/list/RequestQuotaListOutput.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.callv2.drive.application.member.quota.request.list; - -import java.time.Instant; - -import com.callv2.drive.domain.member.QuotaRequestPreview; -import com.callv2.drive.domain.member.QuotaUnit; - -public record RequestQuotaListOutput( - String memberId, - long amount, - QuotaUnit unit, - Instant requestedAt) { - - public static RequestQuotaListOutput from(final QuotaRequestPreview quotaRequestPreview) { - return new RequestQuotaListOutput( - quotaRequestPreview.memberId(), - quotaRequestPreview.amount(), - quotaRequestPreview.unit(), - quotaRequestPreview.requestedAt()); - } - -} diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java index 03a58303..5dacc890 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/DefaultGetQuotaUseCase.java @@ -21,11 +21,11 @@ public DefaultGetQuotaUseCase(final MemberGateway memberGateway, final FileGatew @Override public GetQuotaOutput execute(GetQuotaInput input) { - final MemberID ownerId = MemberID.of(input.id()); + final MemberID ownerId = MemberID.of(input.memberId()); final Member owner = memberGateway .findById(ownerId) - .orElseThrow(() -> NotFoundException.with(Member.class, input.id().toString())); + .orElseThrow(() -> NotFoundException.with(Member.class, input.memberId().toString())); final Long actualUsedQuota = fileGateway .findByOwner(ownerId) @@ -36,7 +36,7 @@ public GetQuotaOutput execute(GetQuotaInput input) { final Long available = owner.getQuota().sizeInBytes() - actualUsedQuota; - return GetQuotaOutput.from(actualUsedQuota, owner.getQuota().sizeInBytes(), available); + return GetQuotaOutput.from(ownerId.getValue(), actualUsedQuota, owner.getQuota().sizeInBytes(), available); } } diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaInput.java b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaInput.java index d20e9b0f..b8d6adad 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaInput.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaInput.java @@ -1,9 +1,9 @@ package com.callv2.drive.application.member.quota.retrieve.get; -public record GetQuotaInput(String id) { +public record GetQuotaInput(String memberId) { - public static GetQuotaInput of(String id) { - return new GetQuotaInput(id); + public static GetQuotaInput of(final String memberId) { + return new GetQuotaInput(memberId); } } diff --git a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaOutput.java b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaOutput.java index cf401a69..bf305426 100644 --- a/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaOutput.java +++ b/application/src/main/java/com/callv2/drive/application/member/quota/retrieve/get/GetQuotaOutput.java @@ -1,9 +1,9 @@ package com.callv2.drive.application.member.quota.retrieve.get; -public record GetQuotaOutput(Long used, Long total, Long available) { +public record GetQuotaOutput(String memberId, Long used, Long total, Long available) { - public static GetQuotaOutput from(final Long used, final Long total, final Long available) { - return new GetQuotaOutput(used, total, available); + public static GetQuotaOutput from(final String memberId, final Long used, final Long total, final Long available) { + return new GetQuotaOutput(memberId, used, total, available); } } diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java new file mode 100644 index 00000000..21b46ab9 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCase.java @@ -0,0 +1,47 @@ +package com.callv2.drive.application.member.synchronize; + +import java.util.Objects; + +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; +import com.callv2.drive.domain.member.Username; + +public class DefaultSynchronizeMemberUseCase extends SynchronizeMemberUseCase { + + private final MemberGateway memberGateway; + + public DefaultSynchronizeMemberUseCase(final MemberGateway memberGateway) { + this.memberGateway = Objects.requireNonNull(memberGateway); + } + + @Override + public void execute(final SynchronizeMemberInput input) { + + final MemberID memberId = MemberID.of(input.id()); + final Username username = Username.of(input.username()); + final Nickname nickname = Nickname.of(input.nickname()); + + final var updatedMember = Member.with( + memberId, + username, + nickname, + null, + null, + input.hasSystemAccess(), + input.createdAt(), + input.updatedAt(), + input.synchronizedVersion()); + + this.memberGateway + .findById(memberId) + .map(member -> member.synchronize(updatedMember)) + .ifPresentOrElse(this::update, () -> memberGateway.create(updatedMember)); + } + + private void update(final Member member) { + memberGateway.update(member.synchronize(member)); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java new file mode 100644 index 00000000..27b0c094 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberInput.java @@ -0,0 +1,25 @@ +package com.callv2.drive.application.member.synchronize; + +import java.time.Instant; + +public record SynchronizeMemberInput( + String id, + String username, + String nickname, + Boolean hasSystemAccess, + Instant createdAt, + Instant updatedAt, + Long synchronizedVersion) { + + public static SynchronizeMemberInput from( + final String id, + final String username, + final String nickname, + final Boolean hasSystemAccess, + final Instant createdAt, + final Instant updatedAt, + final Long synchronizedVersion) { + return new SynchronizeMemberInput(id, username, nickname, hasSystemAccess, createdAt, updatedAt, synchronizedVersion); + } + +} diff --git a/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberUseCase.java b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberUseCase.java new file mode 100644 index 00000000..456b4f83 --- /dev/null +++ b/application/src/main/java/com/callv2/drive/application/member/synchronize/SynchronizeMemberUseCase.java @@ -0,0 +1,7 @@ +package com.callv2.drive.application.member.synchronize; + +import com.callv2.drive.application.UnitUseCase; + +public abstract class SynchronizeMemberUseCase extends UnitUseCase { + +} diff --git a/application/src/test/java/com/callv2/drive/application/file/content/get/DefaultGetFileContentUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/content/get/DefaultGetFileContentUseCaseTest.java index ee9d9e82..fa228678 100644 --- a/application/src/test/java/com/callv2/drive/application/file/content/get/DefaultGetFileContentUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/content/get/DefaultGetFileContentUseCaseTest.java @@ -65,8 +65,9 @@ void givenAnInexistentId_whenCallsExecute_shouldThrowNotFoundException() { final var expectedFileId = FileID.unique(); - final var expectedExceptionMessage = "File with id '%s' not found" - .formatted(expectedFileId.getValue().toString()); + final var expectedExceptionMessage = "[File] not found."; + final var expectedErrorsCount = 1; + final var expectedErrorMessage = "[File] with id [%s] not found.".formatted(expectedFileId.getValue()); when(fileGateway.findById(any())) .thenReturn(Optional.empty()); @@ -76,6 +77,8 @@ void givenAnInexistentId_whenCallsExecute_shouldThrowNotFoundException() { final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorsCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(fileGateway, times(1)).findById(any()); verify(fileGateway, times(1)).findById(eq(expectedFileId)); diff --git a/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java index 52eab0f6..4a555b42 100644 --- a/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/create/DefaultCreateFileUseCaseTest.java @@ -14,6 +14,7 @@ import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; +import java.time.Instant; import java.util.List; import java.util.Optional; @@ -37,8 +38,10 @@ import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; import com.callv2.drive.domain.member.Quota; import com.callv2.drive.domain.member.QuotaUnit; +import com.callv2.drive.domain.member.Username; import com.callv2.drive.domain.storage.StorageService; @ExtendWith(MockitoExtension.class) @@ -62,7 +65,16 @@ public class DefaultCreateFileUseCaseTest { @Test void givenAValidParams_whenCallsExecute_thenShouldCreateFile() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -131,7 +143,16 @@ void givenAValidParams_whenCallsExecute_thenShouldCreateFile() { @Test void givenAnInvalidFolderId_whenCallsExecute_thenShouldThrowNotFoundException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -148,7 +169,9 @@ void givenAnInvalidFolderId_whenCallsExecute_thenShouldThrowNotFoundException() final var expectedContent = new ByteArrayInputStream(contentBytes); final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Folder with id '%s' not found" + final var expectedExceptionMessage = "[Folder] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Folder] with id [%s] not found." .formatted(expectedFolderId.getValue()); when(memberGateway.findById(any())) @@ -168,6 +191,8 @@ void givenAnInvalidFolderId_whenCallsExecute_thenShouldThrowNotFoundException() final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(folderGateway, times(1)).findById(any()); verify(folderGateway, times(1)).findById(eq(expectedFolderId)); @@ -192,7 +217,9 @@ void givenAnInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() final var expectedContent = new ByteArrayInputStream(contentBytes); final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Member with id '%s' not found" + final var expectedExceptionMessage = "[Member] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Member] with id [%s] not found." .formatted(expectedOwnerId.getValue()); when(memberGateway.findById(any())) @@ -209,6 +236,8 @@ void givenAnInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); verify(memberGateway, times(1)).findById(any()); verify(memberGateway, times(1)).findById(eq(expectedOwnerId)); @@ -224,7 +253,16 @@ void givenAnInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() @Test void givenAValidParamsWithAlreadyExistingFileNameOnSameFolder_whenCallsExecute_thenShouldThrowValidationException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -282,7 +320,16 @@ void givenAValidParamsWithAlreadyExistingFileNameOnSameFolder_whenCallsExecute_t @Test void givenAValidParams_whenCallsExecuteAndFileGatewayCreateThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -358,7 +405,16 @@ void givenAValidParams_whenCallsExecuteAndFileGatewayCreateThrowsRandomException @Test void givenAValidParams_whenCallsExecuteAndFileGatewayCreateAndContentGatewayDeleteThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -434,7 +490,16 @@ void givenAValidParams_whenCallsExecuteAndFileGatewayCreateAndContentGatewayDele @Test void givenAValidParams_whenCallsExecuteAndContentGatewayStoreThrowsRandomException_thenShouldThrowInternalErrorException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -488,7 +553,16 @@ void givenAValidParams_whenCallsExecuteAndContentGatewayStoreThrowsRandomExcepti @Test void givenAnInvalidFileName_whenCallsExecute_thenShouldThrowValidationException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); @@ -540,7 +614,16 @@ void givenAnInvalidFileName_whenCallsExecute_thenShouldThrowValidationException( @Test void givenAValidParams_whenCallsExecuteAndMemberQuotaIsExceeded_thenShouldThrowsQuotaExceededException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.BYTE)) .approveQuotaRequest(); @@ -556,9 +639,9 @@ void givenAValidParams_whenCallsExecuteAndMemberQuotaIsExceeded_thenShouldThrows final var expectedContent = new ByteArrayInputStream(contentBytes); final var expectedContentSize = (long) contentBytes.length; - final var expectedExceptionMessage = "Quota exceeded"; + final var expectedExceptionMessage = "Quota exceeded."; final var expectedErrorCount = 1; - final var expectedErrorMessage = "You have exceeded your actual quota of 1 bytes"; + final var expectedErrorMessage = "You have exceeded your current quota of 1 BYTE"; when(memberGateway.findById(ownerId)) .thenReturn(Optional.of(owner)); @@ -588,7 +671,16 @@ void givenAValidParams_whenCallsExecuteAndMemberQuotaIsExceeded_thenShouldThrows @Test void givenAnInvalidParamsWithContentTypeNull_whenCallsExecute_thenShouldThrowsValidationException() { - final var owner = Member.create(MemberID.of("owner")) + final var owner = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) .approveQuotaRequest(); diff --git a/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java new file mode 100644 index 00000000..d121b867 --- /dev/null +++ b/application/src/test/java/com/callv2/drive/application/file/delete/DefaultDeleteFileUseCaseTest.java @@ -0,0 +1,236 @@ +package com.callv2.drive.application.file.delete; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.callv2.drive.domain.exception.NotAllowedException; +import com.callv2.drive.domain.exception.NotFoundException; +import com.callv2.drive.domain.file.Content; +import com.callv2.drive.domain.file.File; +import com.callv2.drive.domain.file.FileGateway; +import com.callv2.drive.domain.file.FileID; +import com.callv2.drive.domain.file.FileName; +import com.callv2.drive.domain.folder.FolderID; +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; +import com.callv2.drive.domain.member.Quota; +import com.callv2.drive.domain.member.QuotaUnit; +import com.callv2.drive.domain.member.Username; +import com.callv2.drive.domain.storage.StorageService; + +@ExtendWith(MockitoExtension.class) +public class DefaultDeleteFileUseCaseTest { + + @InjectMocks + DefaultDeleteFileUseCase useCase; + + @Mock + MemberGateway memberGateway; + + @Mock + FileGateway fileGateway; + + @Mock + StorageService storageService; + + @Test + void givenAValidParam_whenCallsExecute_thenShouldDeleteFile() { + + final var deleter = Member.with( + MemberID.of("deleter"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); + + final FileID expectedFileId = FileID.unique(); + final MemberID expectedDeleterId = deleter.getId(); + final FolderID expectedFolderId = FolderID.unique(); + final FileName expectedFileName = FileName.of("file.txt"); + final Content expectedContent = Content.of( + "file.txt", + "text/plain", + 100L); + final Instant expectedCreatedAt = Instant.now().minus( + java.time.Duration.ofDays(1)); + final Instant expectedUpdatedAt = Instant.now(); + final var file = File.with( + expectedFileId, + expectedDeleterId, + expectedFolderId, + expectedFileName, + expectedContent, + expectedCreatedAt, + expectedUpdatedAt); + + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); + + when(fileGateway.findById(expectedFileId)) + .thenReturn(Optional.of(file)); + + final DeleteFileInput input = DeleteFileInput.of( + expectedDeleterId.getValue(), + expectedFileId.getValue()); + + useCase.execute(input); + + verify(memberGateway, times(1)).findById(any()); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); + verify(fileGateway, times(1)).findById(any()); + verify(fileGateway, times(1)).findById(eq(expectedFileId)); + verify(fileGateway, times(1)).deleteById(any()); + verify(fileGateway, times(1)).deleteById(eq(expectedFileId)); + verify(storageService, times(1)).delete(any()); + verify(storageService, times(1)).delete(eq(expectedContent.location())); + } + + @Test + void givenAInvalidMemberId_whenCallsExecute_thenShouldThrowNotFoundException() { + + final MemberID expectedDeleterId = MemberID.of("deleter"); + final FileID expectedFileId = FileID.unique(); + + final String expectedExceptionMessage = "[Member] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Member] with id [%s] not found.".formatted(expectedDeleterId.getValue()); + + when(memberGateway.findById(any())) + .thenReturn(Optional.empty()); + + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.empty()); + + final DeleteFileInput input = DeleteFileInput.of( + expectedDeleterId.getValue(), + expectedFileId.getValue()); + + final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + + verify(memberGateway, times(1)).findById(any()); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); + verify(fileGateway, never()).findById(any()); + verify(fileGateway, never()).deleteById(any()); + verify(storageService, never()).delete(any()); + } + + @Test + void givenAInvalidFileId_whenCallsExecute_thenShouldThrowNotFoundException() { + + final MemberID expectedDeleterId = MemberID.of("deleter"); + final FileID expectedFileId = FileID.unique(); + + final String expectedExceptionMessage = "[File] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[File] with id [%s] not found.".formatted(expectedFileId.getValue()); + + final var deleter = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + true, + Instant.now(), + Instant.now(), + 0L) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); + + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); + + when(fileGateway.findById(expectedFileId)) + .thenReturn(Optional.empty()); + + final var input = DeleteFileInput.of( + expectedDeleterId.getValue(), + expectedFileId.getValue()); + + final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + + verify(memberGateway, times(1)).findById(any()); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); + verify(fileGateway, times(1)).findById(any()); + verify(fileGateway, times(1)).findById(eq(expectedFileId)); + verify(fileGateway, never()).deleteById(any()); + verify(storageService, never()).delete(any()); + + } + + @Test + void givenAValidMemberIdButNotHaveSystemAccess_whenCallsExecute_thenShouldThrowNotFoundException() { + + final MemberID expectedDeleterId = MemberID.of("deleter"); + final FileID expectedFileId = FileID.unique(); + + final String expectedExceptionMessage = "The requested action is not allowed."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "Member does not have permission to delete files."; + + final var deleter = Member.with( + MemberID.of("owner"), + Username.of("username"), + Nickname.of("nickname"), + Quota.of(0, QuotaUnit.BYTE), + null, + false, + Instant.now(), + Instant.now(), + 0L) + .requestQuota(Quota.of(1, QuotaUnit.GIGABYTE)) + .approveQuotaRequest(); + + when(memberGateway.findById(expectedDeleterId)) + .thenReturn(Optional.of(deleter)); + + final var input = DeleteFileInput.of( + expectedDeleterId.getValue(), + expectedFileId.getValue()); + + final var actualException = assertThrows(NotAllowedException.class, () -> useCase.execute(input)); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + + verify(memberGateway, times(1)).findById(any()); + verify(memberGateway, times(1)).findById(eq(expectedDeleterId)); + verify(fileGateway, never()).findById(any()); + verify(fileGateway, never()).deleteById(any()); + verify(storageService, never()).delete(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 6f93cbd9..c5f46bee 100644 --- a/application/src/test/java/com/callv2/drive/application/file/retrieve/get/DefaultGetFileUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/file/retrieve/get/DefaultGetFileUseCaseTest.java @@ -76,9 +76,9 @@ void givenAInvalidId_whenCallsExecute_thenShouldReturnNotFound() { final var expectedId = FileID.unique(); - final var expectedExceptionMessage = "File with id '%s' not found".formatted(expectedId.getValue().toString()); + final var expectedExceptionMessage = "[File] not found."; final var expectedErrorsCount = 1; - final var expectedErrorMessage = "File with id '%s' not found".formatted(expectedId.getValue().toString()); + final var expectedErrorMessage = "[File] with id [%s] not found.".formatted(expectedId.getValue().toString()); when(fileGateway.findById(any())) .thenReturn(Optional.empty()); diff --git a/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java index e079f46e..574eaf7a 100644 --- a/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/folder/move/DefaultMoveFolderUseCaseTest.java @@ -1,12 +1,16 @@ package com.callv2.drive.application.folder.move; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.doNothing; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,9 +51,8 @@ void givenVAlidInput_whenCallsExecute_thenMoveFolder() { when(folderGateway.findById(expectedRootFolder.getId())) .thenReturn(Optional.of(expectedRootFolder)); - doNothing() - .when(folderGateway) - .updateAll(anyList()); + when(folderGateway.findByParentFolderId(expectedFolderTarget.getId())) + .thenReturn(Set.of()); final var input = new MoveFolderInput( expectedFolderToMove.getId().getValue(), @@ -57,7 +60,10 @@ void givenVAlidInput_whenCallsExecute_thenMoveFolder() { assertDoesNotThrow(() -> useCase.execute(input)); - verify(folderGateway).updateAll(anyList()); + verify(folderGateway, never()).updateAll(anyList()); + verify(folderGateway, times(1)).update(eq(expectedFolderToMove)); + verify(folderGateway, times(1)).findByParentFolderId(any()); + } } diff --git a/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java index 68e524fc..2a89f6b2 100644 --- a/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java +++ b/application/src/test/java/com/callv2/drive/application/folder/retrieve/get/DefaultGetFolderUseCaseTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.when; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,72 +29,92 @@ @ExtendWith(MockitoExtension.class) public class DefaultGetFolderUseCaseTest { - @InjectMocks - DefaultGetFolderUseCase useCase; + @InjectMocks + DefaultGetFolderUseCase useCase; - @Mock - FolderGateway folderGateway; + @Mock + FolderGateway folderGateway; - @Mock - FileGateway fileGateway; + @Mock + FileGateway fileGateway; - @Test - void givenAValidFolderId_whenCallsExecute_thenShouldReturnFolder() { + @Test + void givenAValidFolderId_whenCallsExecute_thenShouldReturnFolder() { - final var ownerId = MemberID.of("owner"); + final var ownerId = MemberID.of("owner"); - final var expectedFolderName = "folder"; - final var expectedFolder = Folder.create( - ownerId, - FolderName.of(expectedFolderName), - Folder.createRoot(ownerId)); + final var expectedFolderName = "folder"; + final var expectedFolder = Folder.create( + ownerId, + FolderName.of(expectedFolderName), + Folder.createRoot(ownerId)); - final var expectedFolderId = expectedFolder.getId(); - final var expectedSubFolders = expectedFolder.getSubFolders(); - final var expectedCreatedAt = expectedFolder.getCreatedAt(); - final var expectedUpdatedAt = expectedFolder.getUpdatedAt(); - final var expectedDeletedAt = expectedFolder.getDeletedAt(); + final var expectedSubFolder1 = Folder.create( + ownerId, + FolderName.of("subFolder1"), + expectedFolder); + final var expectedSubFolder2 = Folder.create( + ownerId, + FolderName.of("subFolder2"), + expectedFolder); - when(folderGateway.findById(expectedFolderId)) - .thenReturn(Optional.of(expectedFolder)); + final var expectedSubFolders = Set.of(expectedSubFolder1, expectedSubFolder2); - final var input = GetFolderInput.with(expectedFolderId.getValue()); + final var expectedFolderId = expectedFolder.getId(); + final var expectedCreatedAt = expectedFolder.getCreatedAt(); + final var expectedUpdatedAt = expectedFolder.getUpdatedAt(); + final var expectedDeletedAt = expectedFolder.getDeletedAt(); - final var actualOutput = assertDoesNotThrow(() -> useCase.execute(input)); + when(folderGateway.findById(expectedFolderId)) + .thenReturn(Optional.of(expectedFolder)); - assertEquals(expectedFolderId.getValue(), actualOutput.id()); - assertEquals(expectedFolderName, actualOutput.name()); - assertEquals(expectedFolder.getParentFolder().getValue(), actualOutput.parentFolder()); - assertEquals(expectedSubFolders.size(), actualOutput.subFolders().size()); - assertEquals(expectedCreatedAt, actualOutput.createdAt()); - assertEquals(expectedUpdatedAt, actualOutput.updatedAt()); - assertEquals(expectedDeletedAt, actualOutput.deletedAt()); + when(folderGateway.findByParentFolderId(expectedFolder.getId())) + .thenReturn(expectedSubFolders); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + final var input = GetFolderInput.with(expectedFolderId.getValue()); - } + final var actualOutput = assertDoesNotThrow(() -> useCase.execute(input)); - @Test - void givenNotExistentFolderId_whenCallsExecute_thenShouldThorwsNotFoundException() { + assertEquals(expectedFolderId.getValue(), actualOutput.id()); + assertEquals(expectedFolderName, actualOutput.name()); + assertEquals(expectedFolder.getParentFolder().getValue(), actualOutput.parentFolder()); + assertEquals(expectedSubFolders.size(), actualOutput.subFolders().size()); + assertEquals(expectedCreatedAt, actualOutput.createdAt()); + assertEquals(expectedUpdatedAt, actualOutput.updatedAt()); + assertEquals(expectedDeletedAt, actualOutput.deletedAt()); - final var expectedFolderId = FolderID.unique(); + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); - final var excpectedExceptionMessage = "Folder with id '%s' not found" - .formatted(expectedFolderId.getValue().toString()); + verify(folderGateway, times(1)).findByParentFolderId(any()); + verify(folderGateway, times(1)).findByParentFolderId(eq(expectedFolder.getId())); - when(folderGateway.findById(expectedFolderId)) - .thenReturn(Optional.empty()); + } - final var input = GetFolderInput.with(expectedFolderId.getValue()); + @Test + void givenNotExistentFolderId_whenCallsExecute_thenShouldThorwsNotFoundException() { - final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + final var expectedFolderId = FolderID.unique(); - assertEquals(excpectedExceptionMessage, actualException.getMessage()); + final var expectedExceptionMessage = "[Folder] not found."; + final var expectedErrorCount = 1; + final var expectedErrorMessage = "[Folder] with id [%s] not found." + .formatted(expectedFolderId.getValue()); - verify(folderGateway, times(1)).findById(any()); - verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + when(folderGateway.findById(expectedFolderId)) + .thenReturn(Optional.empty()); - } + final var input = GetFolderInput.with(expectedFolderId.getValue()); + + final var actualException = assertThrows(NotFoundException.class, () -> useCase.execute(input)); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals(expectedErrorCount, actualException.getErrors().size()); + assertEquals(expectedErrorMessage, actualException.getErrors().get(0).message()); + + verify(folderGateway, times(1)).findById(any()); + verify(folderGateway, times(1)).findById(eq(expectedFolderId)); + + } } diff --git a/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java b/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java new file mode 100644 index 00000000..70e250ee --- /dev/null +++ b/application/src/test/java/com/callv2/drive/application/member/synchronize/DefaultSynchronizeMemberUseCaseTest.java @@ -0,0 +1,152 @@ +package com.callv2.drive.application.member.synchronize; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.callv2.drive.domain.member.Member; +import com.callv2.drive.domain.member.MemberGateway; +import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; +import com.callv2.drive.domain.member.Quota; +import com.callv2.drive.domain.member.QuotaUnit; +import com.callv2.drive.domain.member.Username; + +@ExtendWith(MockitoExtension.class) +public class DefaultSynchronizeMemberUseCaseTest { + + @InjectMocks + DefaultSynchronizeMemberUseCase useCase; + + @Mock + MemberGateway memberGateway; + + @Test + void givenAnValidInput_whenCallsExecute_thenShouldSynchonizeMember() { + + final var expectedIdVAlue = "123"; + final var expectedUsernameValue = "username"; + final var expectedNicknameValue = "nickname"; + final var expectedCreatedAt = java.time.Instant.now(); + final var expectedUpdatedAt = expectedCreatedAt.plusSeconds(100); + final var expectedSynchronizedVersion = 1L; + + final var expectedMemberId = MemberID.of(expectedIdVAlue); + final var expectedUsername = Username.of(expectedUsernameValue); + final var expectedNickname = Nickname.of(expectedNicknameValue); + final var expectedQuota = Quota.of(10L, QuotaUnit.GIGABYTE); + final var expectedHasSystemAccess = true; + + final var oldMember = Member.with( + expectedMemberId, + expectedUsername, + expectedNickname, + expectedQuota, + null, + false, + expectedCreatedAt, + expectedCreatedAt, + 0L); + + when(memberGateway.findById(expectedMemberId)) + .thenReturn(Optional.of(oldMember)); + + when(memberGateway.update(any())) + .thenAnswer(returnsFirstArg()); + + final var input = SynchronizeMemberInput.from( + expectedIdVAlue, + expectedUsernameValue, + expectedNicknameValue, + expectedHasSystemAccess, + expectedCreatedAt, + expectedUpdatedAt, + expectedSynchronizedVersion); + + assertDoesNotThrow(() -> useCase.execute(input)); + + verify(memberGateway, times(1)).findById(eq(expectedMemberId)); + verify(memberGateway, times(1)).findById(any()); + + verify(memberGateway).update(argThat(member -> { + assertEquals(expectedMemberId, member.getId()); + assertEquals(expectedUsername, member.getUsername()); + assertEquals(expectedNickname, member.getNickname()); + assertEquals(expectedQuota, member.getQuota()); + assertEquals(expectedHasSystemAccess, member.hasSystemAccess()); + assertEquals(expectedCreatedAt, member.getCreatedAt()); + assertEquals(expectedUpdatedAt, member.getUpdatedAt()); + assertEquals(expectedSynchronizedVersion, member.getSynchronizedVersion()); + return true; + })); + verify(memberGateway, times(1)).update(any()); + + verify(memberGateway, times(0)).create(any()); + } + + @Test + void givenAnValidInputWhitNonExistentMember_whenCallsExecute_thenShouldCreateMember() { + + final var expectedIdVAlue = "123"; + final var expectedUsernameValue = "username"; + final var expectedNicknameValue = "nickname"; + final var expectedCreatedAt = java.time.Instant.now(); + final var expectedUpdatedAt = expectedCreatedAt.plusSeconds(100); + final var expectedSynchronizedVersion = 1L; + + final var expectedMemberId = MemberID.of(expectedIdVAlue); + final var expectedUsername = Username.of(expectedUsernameValue); + final var expectedNickname = Nickname.of(expectedNicknameValue); + final var expectedQuota = Quota.of(0L, QuotaUnit.BYTE); + final var expectedHasSystemAccess = false; + + when(memberGateway.findById(expectedMemberId)) + .thenReturn(Optional.empty()); + + when(memberGateway.create(any())) + .thenAnswer(returnsFirstArg()); + + final var input = SynchronizeMemberInput.from( + expectedIdVAlue, + expectedUsernameValue, + expectedNicknameValue, + expectedHasSystemAccess, + expectedCreatedAt, + expectedUpdatedAt, + expectedSynchronizedVersion); + + assertDoesNotThrow(() -> useCase.execute(input)); + + verify(memberGateway, times(1)).findById(eq(expectedMemberId)); + verify(memberGateway, times(1)).findById(any()); + + verify(memberGateway, times(0)).update(any()); + + verify(memberGateway, times(1)).create(argThat(member -> { + assertEquals(expectedMemberId, member.getId()); + assertEquals(expectedUsername, member.getUsername()); + assertEquals(expectedNickname, member.getNickname()); + assertEquals(expectedQuota, member.getQuota()); + assertEquals(expectedCreatedAt, member.getCreatedAt()); + assertEquals(expectedUpdatedAt, member.getUpdatedAt()); + assertEquals(expectedSynchronizedVersion, member.getSynchronizedVersion()); + return true; + })); + verify(memberGateway, times(1)).create(any()); + } + +} diff --git a/domain/build.gradle b/domain/build.gradle index 2fe4511b..fe967bc4 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -10,11 +10,10 @@ repositories { } dependencies { - testImplementation libs.junit.jupiter + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation libs.guava } java { diff --git a/domain/src/main/java/com/callv2/drive/domain/Entity.java b/domain/src/main/java/com/callv2/drive/domain/Entity.java index b201ac1a..67a38a38 100644 --- a/domain/src/main/java/com/callv2/drive/domain/Entity.java +++ b/domain/src/main/java/com/callv2/drive/domain/Entity.java @@ -2,9 +2,7 @@ import java.util.Objects; -import com.callv2.drive.domain.validation.ValidationHandler; - -public abstract class Entity> { +public abstract class Entity> implements Validatable { protected final ID id; @@ -12,8 +10,6 @@ protected Entity(final ID id) { this.id = Objects.requireNonNull(id, "'id' should not be null"); } - public abstract void validate(ValidationHandler handler); - public ID getId() { return id; } diff --git a/domain/src/main/java/com/callv2/drive/domain/Identifier.java b/domain/src/main/java/com/callv2/drive/domain/Identifier.java index b53c9437..4dc7a04f 100644 --- a/domain/src/main/java/com/callv2/drive/domain/Identifier.java +++ b/domain/src/main/java/com/callv2/drive/domain/Identifier.java @@ -2,7 +2,7 @@ import java.util.Objects; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public abstract class Identifier implements ValueObject { @@ -18,9 +18,9 @@ public T getValue() { } @Override - public void validate(ValidationHandler aHandler) { + public void validate(ValidationHandler handler) { if (Objects.isNull(this.id)) - aHandler.append(new Error("'id' should not be null")); + handler.append(new ValidationError("'id' should not be null")); } @Override diff --git a/domain/src/main/java/com/callv2/drive/domain/Validatable.java b/domain/src/main/java/com/callv2/drive/domain/Validatable.java new file mode 100644 index 00000000..1e62a60b --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/Validatable.java @@ -0,0 +1,10 @@ +package com.callv2.drive.domain; + +import com.callv2.drive.domain.validation.ValidationHandler; + +@FunctionalInterface +public interface Validatable { + + void validate(ValidationHandler handler); + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/ValueObject.java b/domain/src/main/java/com/callv2/drive/domain/ValueObject.java index f95bd954..374aa8d5 100644 --- a/domain/src/main/java/com/callv2/drive/domain/ValueObject.java +++ b/domain/src/main/java/com/callv2/drive/domain/ValueObject.java @@ -2,9 +2,9 @@ import com.callv2.drive.domain.validation.ValidationHandler; -public interface ValueObject { +public interface ValueObject extends Validatable { - default void validate(ValidationHandler aHandler) { + default void validate(ValidationHandler handler) { }; } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java b/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java new file mode 100644 index 00000000..3efe5bf1 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/AlreadyExistsException.java @@ -0,0 +1,23 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +import com.callv2.drive.domain.Entity; + +public class AlreadyExistsException extends SilentDomainException { + + private AlreadyExistsException( + final Class> entityClass, + final String id) { + super("[%s] already exists.".formatted(entityClass.getSimpleName()), + List.of(DomainException.Error + .with("[%s] with id [%s] already exists.".formatted(entityClass.getSimpleName())))); + } + + public static AlreadyExistsException with( + final Class> entityClass, + final String id) { + return new AlreadyExistsException(entityClass, id); + } + +} \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/DomainException.java b/domain/src/main/java/com/callv2/drive/domain/exception/DomainException.java index 77b742b5..c52631e4 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/DomainException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/DomainException.java @@ -1,28 +1,62 @@ package com.callv2.drive.domain.exception; +import java.util.ArrayList; import java.util.List; -import com.callv2.drive.domain.validation.Error; +public abstract class DomainException extends RuntimeException { -public class DomainException extends NoStacktraceException { + private final List errors; - protected final List errors; + protected DomainException( + final String message, + final List errors, + final Throwable cause, + final boolean verbose) { + super(message, cause, enableSuppression(verbose), writableStackTrace(verbose)); + this.errors = addCauseToErrors(errors, cause) == null ? List.of() : new ArrayList<>(errors); + } + + public List getErrors() { + return List.copyOf(errors); + } - protected DomainException(final String aMessage, final List anErrors) { - super(aMessage); - this.errors = anErrors; + public String errorsToString() { + return "[" + errors.stream() + .map(DomainException.Error::message) + .reduce((a, b) -> a + ", " + b) + .orElse("No errors") + "]"; } - public static DomainException with(final Error anError) { - return new DomainException(anError.message(), List.of(anError)); + private static boolean enableSuppression(final boolean verbose) { + return verbose ? false : true; } - public static DomainException with(final List anErrors) { - return new DomainException("", anErrors); + private static boolean writableStackTrace(final boolean verbose) { + return verbose ? true : false; } - public List getErrors() { - return errors; + private static List addCauseToErrors( + final List errors, + final Throwable cause) { + if (cause == null) + return errors; + + final List result = new ArrayList<>(errors != null ? errors : List.of()); + result.add(DomainException.Error.with(cause)); + return result; + } + + public record Error(String message) { + + public static DomainException.Error with(final String message) { + return new DomainException.Error(message); + } + + public static DomainException.Error with(final Throwable cause) { + return new DomainException.Error( + "Exception:[" + cause.getClass().getName() + "] Message:[" + cause.getMessage() + "]"); + } + } -} +} \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java b/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java new file mode 100644 index 00000000..f43270e0 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/IdMismatchException.java @@ -0,0 +1,23 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +import com.callv2.drive.domain.Entity; + +public class IdMismatchException extends SilentDomainException { + + private IdMismatchException( + final Class> entityClass, + final String id) { + super("[%s]'s id doesn't match.".formatted(entityClass.getSimpleName()), + List.of(DomainException.Error + .with("[%s] with id [%s] doesn't match.".formatted(entityClass.getSimpleName(), id)))); + } + + public static IdMismatchException with( + final Class> entityClass, + final String id) { + return new IdMismatchException(entityClass, id); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/InternalErrorException.java b/domain/src/main/java/com/callv2/drive/domain/exception/InternalErrorException.java index c28ebfdd..686111bf 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/InternalErrorException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/InternalErrorException.java @@ -1,13 +1,29 @@ package com.callv2.drive.domain.exception; -public class InternalErrorException extends NoStacktraceException { +import java.util.List; - protected InternalErrorException(final String aMessage, final Throwable cause) { - super(aMessage, cause); +public class InternalErrorException extends VerboseDomainException { + + protected InternalErrorException( + final String message, + final List errors, + final Throwable cause) { + super(message, errors, cause); + } + + public static InternalErrorException with(final Throwable cause) { + return new InternalErrorException(cause.getMessage(), List.of(), cause); + } + + public static InternalErrorException with(final String message, final Throwable cause) { + return new InternalErrorException(message, List.of(), cause); } - public static InternalErrorException with(final String aMessage, final Throwable cause) { - return new InternalErrorException(aMessage, cause); + public static InternalErrorException with( + final String message, + final List errors, + final Throwable cause) { + return new InternalErrorException(message, errors, cause); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/NoStacktraceException.java b/domain/src/main/java/com/callv2/drive/domain/exception/NoStacktraceException.java deleted file mode 100644 index 57aa8e1d..00000000 --- a/domain/src/main/java/com/callv2/drive/domain/exception/NoStacktraceException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.callv2.drive.domain.exception; - -public abstract class NoStacktraceException extends RuntimeException { - - protected NoStacktraceException(final String message) { - this(message, null); - } - - protected NoStacktraceException(final String message, final Throwable cause) { - super(message, cause, true, false); - } - -} diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java b/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java new file mode 100644 index 00000000..ce0db28d --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/NotAllowedException.java @@ -0,0 +1,17 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +public class NotAllowedException extends SilentDomainException { + + private NotAllowedException(final List actions) { + super( + "The requested action is not allowed.", + actions.stream().map(DomainException.Error::with).toList()); + } + + public static NotAllowedException with(final String action) { + final List list = action == null ? List.of() : List.of(action); + return new NotAllowedException(list); + } +} diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/NotFoundException.java b/domain/src/main/java/com/callv2/drive/domain/exception/NotFoundException.java index 99f6f90a..aced298a 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/NotFoundException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/NotFoundException.java @@ -3,35 +3,21 @@ import java.util.List; import com.callv2.drive.domain.Entity; -import com.callv2.drive.domain.Identifier; -import com.callv2.drive.domain.validation.Error; -public class NotFoundException extends DomainException { +public class NotFoundException extends SilentDomainException { - private NotFoundException(final String message) { - super(message, List.of(Error.with(message))); - } - - public static NotFoundException with( - final Class>> entityClass) { - - final String message = String.format( - "%s not found", - entityClass.getSimpleName()); - - return new NotFoundException(message); + private NotFoundException( + final Class> entityClass, + final String id) { + super("[%s] not found.".formatted(entityClass.getSimpleName()), + List.of(DomainException.Error + .with("[%s] with id [%s] not found.".formatted(entityClass.getSimpleName(), id)))); } public static NotFoundException with( - final Class>> entityClass, + final Class> entityClass, final String id) { - - final String message = String.format( - "%s with id '%s' not found", - entityClass.getSimpleName(), - id); - - return new NotFoundException(message); + return new NotFoundException(entityClass, id); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/QuotaExceededException.java b/domain/src/main/java/com/callv2/drive/domain/exception/QuotaExceededException.java index 97c23948..e462be34 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/QuotaExceededException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/QuotaExceededException.java @@ -2,15 +2,20 @@ import java.util.List; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.member.Quota; -public class QuotaExceededException extends DomainException { +public class QuotaExceededException extends SilentDomainException { - private QuotaExceededException(final String aMessage, final List anErrors) { - super(aMessage, List.copyOf(anErrors)); + private QuotaExceededException(final Quota actualQuota) { + super( + "Quota exceeded.", + List.of(DomainException.Error.with("You have exceeded your current quota of " + + actualQuota.amount() + + " " + actualQuota.unit().name()))); } - public static QuotaExceededException with(final String message, final Error error) { - return new QuotaExceededException(message, List.of(error)); + public static QuotaExceededException with(final Quota actualQuota) { + return new QuotaExceededException(actualQuota); } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/SilentDomainException.java b/domain/src/main/java/com/callv2/drive/domain/exception/SilentDomainException.java new file mode 100644 index 00000000..6b14ff26 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/SilentDomainException.java @@ -0,0 +1,17 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +public abstract class SilentDomainException extends DomainException { + + private static final boolean VERBOSE = false; + + protected SilentDomainException(String message, List errors) { + super(message, errors, null, VERBOSE); + } + + protected SilentDomainException(String message, List errors, Throwable cause) { + super(message, errors, cause, VERBOSE); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java b/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java new file mode 100644 index 00000000..a958291c --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/SynchronizedVersionOutdatedException.java @@ -0,0 +1,26 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +import com.callv2.drive.domain.Entity; + +public class SynchronizedVersionOutdatedException extends SilentDomainException { + + private SynchronizedVersionOutdatedException( + final Class> entityClass, + final Long actualEntityVersion, + final Long outdatedVersionProvided) { + super( + "Outdated version provided", + List.of(DomainException.Error.with("[%s] with actual version [%d] is outdated, but [%d] was provided." + .formatted(entityClass.getSimpleName(), actualEntityVersion, outdatedVersionProvided)))); + } + + public static SynchronizedVersionOutdatedException with( + final Class> entityClass, + final Long actualEntityVersion, + final Long outdatedVersionProvided) { + return new SynchronizedVersionOutdatedException(entityClass, actualEntityVersion, outdatedVersionProvided); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/ValidationException.java b/domain/src/main/java/com/callv2/drive/domain/exception/ValidationException.java index 2cee5da9..04a83af1 100644 --- a/domain/src/main/java/com/callv2/drive/domain/exception/ValidationException.java +++ b/domain/src/main/java/com/callv2/drive/domain/exception/ValidationException.java @@ -2,21 +2,40 @@ import java.util.List; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; -public class ValidationException extends DomainException { +public class ValidationException extends SilentDomainException { - private ValidationException(final String aMessage, final List anErrors) { - super(aMessage, List.copyOf(anErrors)); + private ValidationException(final String message, final List errors) { + super(message, List.copyOf(errors)); } - public static ValidationException with(final String aMessage, final Notification aNotification) { - return new ValidationException(aMessage, List.copyOf(aNotification.getErrors())); + public static ValidationException with(final String message, final Notification aNotification) { + return new ValidationException( + message, + aNotification + .getErrors() + .stream() + .map(ValidationError::toDomainError) + .toList()); } - public static ValidationException with(final String message, final Error error) { + public static ValidationException with(final String message, final DomainException.Error error) { return new ValidationException(message, List.of(error)); } + public static ValidationException with(final String message, final ValidationError error) { + return new ValidationException(message, List.of(error.toDomainError())); + } + + public static ValidationException with(final String message, final List errors) { + return new ValidationException( + message, + errors + .stream() + .map(ValidationError::toDomainError) + .toList()); + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/exception/VerboseDomainException.java b/domain/src/main/java/com/callv2/drive/domain/exception/VerboseDomainException.java new file mode 100644 index 00000000..658b2720 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/exception/VerboseDomainException.java @@ -0,0 +1,13 @@ +package com.callv2.drive.domain.exception; + +import java.util.List; + +public class VerboseDomainException extends DomainException { + + private static final boolean VERBOSE = true; + + protected VerboseDomainException(String message, List errors, Throwable cause) { + super(message, errors, cause, VERBOSE); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/file/Content.java b/domain/src/main/java/com/callv2/drive/domain/file/Content.java index 692df344..b63b1b6e 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/Content.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/Content.java @@ -2,7 +2,7 @@ import com.callv2.drive.domain.ValueObject; import com.callv2.drive.domain.validation.ValidationHandler; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; public record Content(String location, String type, long size) implements ValueObject { @@ -18,24 +18,24 @@ public void validate(final ValidationHandler aHandler) { private void validateLocation(final ValidationHandler aHandler) { if (location == null) { - aHandler.append(new Error("'location' cannot be null.")); + aHandler.append(new ValidationError("'location' cannot be null.")); return; } if (location.trim().isEmpty()) { - aHandler.append(new Error("'location' cannot be empty.")); + aHandler.append(new ValidationError("'location' cannot be empty.")); return; } } private void validateType(final ValidationHandler aHandler) { if (type == null) { - aHandler.append(new Error("'type' cannot be null.")); + aHandler.append(new ValidationError("'type' cannot be null.")); return; } if (type.trim().isEmpty()) { - aHandler.append(new Error("'type' cannot be empty.")); + aHandler.append(new ValidationError("'type' cannot be empty.")); return; } } diff --git a/domain/src/main/java/com/callv2/drive/domain/file/File.java b/domain/src/main/java/com/callv2/drive/domain/file/File.java index 99f8679d..cdfdd044 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/File.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/File.java @@ -136,4 +136,10 @@ private void selfValidate() { throw ValidationException.with("Validation fail has occoured", notification); } + @Override + public String toString() { + return "File [id=" + id + ", owner=" + owner + ", folder=" + folder + ", name=" + name + ", content=" + content + + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + "]"; + } + } \ No newline at end of file diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java b/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java index 11fa6118..46064f11 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileGateway.java @@ -20,4 +20,6 @@ public interface FileGateway { Page findAll(SearchQuery searchQuery); + void deleteById(FileID id); + } diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileID.java b/domain/src/main/java/com/callv2/drive/domain/file/FileID.java index 9bd05546..3d8de895 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileID.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileID.java @@ -18,4 +18,9 @@ public static FileID unique() { return FileID.of(UUID.randomUUID()); } + @Override + public String toString() { + return "FileID [value=" + getValue() + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileName.java b/domain/src/main/java/com/callv2/drive/domain/file/FileName.java index 9f8acb30..9b2ab8bf 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileName.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileName.java @@ -1,7 +1,7 @@ package com.callv2.drive.domain.file; import com.callv2.drive.domain.ValueObject; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public record FileName(String value) implements ValueObject { @@ -30,22 +30,22 @@ public static FileName of(final String value) { public void validate(final ValidationHandler aHandler) { if (value == null) { - aHandler.append(new Error("'name' cannot be null.")); + aHandler.append(new ValidationError("'name' cannot be null.")); return; } if (value.trim().isEmpty()) { - aHandler.append(new Error("'name' cannot be empty.")); + aHandler.append(new ValidationError("'name' cannot be empty.")); return; } String trimmedName = value.trim(); if (trimmedName.length() < MIN_LENGTH || trimmedName.length() > MAX_LENGTH) aHandler.append( - new Error("'name' must be between " + MIN_LENGTH + " and " + MAX_LENGTH + " characters.")); + new ValidationError("'name' must be between " + MIN_LENGTH + " and " + MAX_LENGTH + " characters.")); if (isReservedName(trimmedName)) - aHandler.append(new Error("'name' cannot be a reserved name: " + trimmedName)); + aHandler.append(new ValidationError("'name' cannot be a reserved name: " + trimmedName)); } private boolean isReservedName(final String name) { diff --git a/domain/src/main/java/com/callv2/drive/domain/file/FileValidator.java b/domain/src/main/java/com/callv2/drive/domain/file/FileValidator.java index be3de072..7c4ed141 100644 --- a/domain/src/main/java/com/callv2/drive/domain/file/FileValidator.java +++ b/domain/src/main/java/com/callv2/drive/domain/file/FileValidator.java @@ -1,6 +1,6 @@ package com.callv2.drive.domain.file; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.Validator; @@ -23,7 +23,7 @@ public void validate() { private void validateId() { if (this.file.getId() == null) { - this.validationHandler().append(new Error("'id' should not be null")); + this.validationHandler().append(new ValidationError("'id' should not be null")); return; } @@ -33,7 +33,7 @@ private void validateId() { private void validateName() { if (this.file.getName() == null) { - this.validationHandler().append(new Error("'name' should not be null")); + this.validationHandler().append(new ValidationError("'name' should not be null")); return; } @@ -43,7 +43,7 @@ private void validateName() { private void validateContent() { if (this.file.getContent() == null) { - this.validationHandler().append(new Error("'content' should not be null")); + this.validationHandler().append(new ValidationError("'content' should not be null")); return; } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java b/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java index 39a42fda..9e1bc8fa 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/Folder.java @@ -1,13 +1,11 @@ package com.callv2.drive.domain.folder; import java.time.Instant; -import java.util.HashSet; -import java.util.Set; import com.callv2.drive.domain.AggregateRoot; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.member.MemberID; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.handler.Notification; @@ -19,7 +17,6 @@ public class Folder extends AggregateRoot { private FolderName name; private FolderID parentFolder; - private Set subFolders; private Instant createdAt; private Instant updatedAt; @@ -30,7 +27,6 @@ private Folder( final MemberID owner, final FolderName name, final FolderID parentFolder, - final Set subFolders, final Instant createdAt, final Instant updatedAt, final Instant deletedAt, @@ -40,7 +36,6 @@ private Folder( this.owner = owner; this.name = name; this.parentFolder = parentFolder; - this.subFolders = subFolders == null ? new HashSet<>() : new HashSet<>(subFolders); this.createdAt = createdAt; this.updatedAt = updatedAt; this.deletedAt = deletedAt; @@ -54,12 +49,11 @@ public static Folder with( final MemberID owner, final FolderName name, final FolderID parentFolder, - final Set subFolders, final Instant createdAt, final Instant updatedAt, final Instant deletedAt, final boolean rootFolder) { - return new Folder(id, owner, name, parentFolder, subFolders, createdAt, updatedAt, deletedAt, rootFolder); + return new Folder(id, owner, name, parentFolder, createdAt, updatedAt, deletedAt, rootFolder); } public static Folder createRoot(final MemberID owner) { @@ -70,7 +64,6 @@ public static Folder createRoot(final MemberID owner) { owner, FolderName.of("Root"), null, - new HashSet<>(), now, now, null, @@ -89,13 +82,11 @@ public static Folder create( owner, name, parentFolder.getId(), - new HashSet<>(), now, now, null, false); - parentFolder.addSubFolder(folder); return folder; } @@ -119,52 +110,16 @@ public Folder changeParentFolder(final Folder parentFolder) { if (this.parentFolder.equals(parentFolder.getId())) return this; - if (this.subFolders.stream().anyMatch(it -> it.id().equals(parentFolder.getId()))) - throw ValidationException.with("Error on change parent folder", - Error.with("Parent folder cannot be a subfolder")); + final Notification notification = Notification.create(); if (this.getId().equals(parentFolder.getId())) - throw ValidationException.with("Error on change parent folder", - Error.with("Parent folder cannot be the same folder")); + notification.append(ValidationError.with("Parent folder cannot be the same folder")); - this.parentFolder = parentFolder.getId(); - this.updatedAt = Instant.now(); - return this; - } - - public Folder addSubFolder(final Folder folder) { - - if (folder == null) - return this; - - final SubFolder subFolder = SubFolder.from(folder); - - if (this.subFolders.contains(subFolder)) - throw ValidationException.with("Error on add subfolder", Error.with("SubFolder already exists")); - - if (this.subFolders.stream().anyMatch(it -> it.name().equals(folder.getName()))) - throw ValidationException.with("Error on add subfolder", - Error.with("SubFolder %s already exists".formatted(folder.getName().value()))); - - this.subFolders.add(subFolder); - this.updatedAt = Instant.now(); - - return this; - } - - public Folder removeSubFolder(final Folder folder) { - - if (folder == null) - return this; - - final SubFolder subFolder = SubFolder.from(folder); - - if (!this.subFolders.contains(subFolder)) - return this; + if (notification.hasError()) + throw ValidationException.with("Error on change parent folder", notification); - this.subFolders.remove(subFolder); + this.parentFolder = parentFolder.getId(); this.updatedAt = Instant.now(); - return this; } @@ -184,10 +139,6 @@ public FolderName getName() { return name; } - public Set getSubFolders() { - return Set.copyOf(subFolders); - } - public Instant getCreatedAt() { return createdAt; } @@ -208,4 +159,11 @@ private void selfValidate() { throw ValidationException.with("Validation fail has occoured", notification); } + @Override + public String toString() { + return "Folder [id=" + id + ", rootFolder=" + rootFolder + ", owner=" + owner + ", name=" + name + + ", parentFolder=" + parentFolder + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + + ", deletedAt=" + deletedAt + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/FolderGateway.java b/domain/src/main/java/com/callv2/drive/domain/folder/FolderGateway.java index 6101dff5..b1fd7ce1 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/FolderGateway.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/FolderGateway.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; import com.callv2.drive.domain.pagination.Page; import com.callv2.drive.domain.pagination.SearchQuery; @@ -10,6 +11,8 @@ public interface FolderGateway { Optional findRoot(); + Set findByParentFolderId(FolderID parentFolderId); + Folder create(Folder folder); Folder update(Folder folder); diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/FolderID.java b/domain/src/main/java/com/callv2/drive/domain/folder/FolderID.java index e5c4d642..eab47e85 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/FolderID.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/FolderID.java @@ -18,4 +18,9 @@ public static FolderID unique() { return FolderID.of(UUID.randomUUID()); } + @Override + public String toString() { + return "FolderID [value=" + getValue() + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/FolderName.java b/domain/src/main/java/com/callv2/drive/domain/folder/FolderName.java index 590acf6e..d0efec0d 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/FolderName.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/FolderName.java @@ -4,7 +4,7 @@ import java.util.regex.Pattern; import com.callv2.drive.domain.ValueObject; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public record FolderName(String value) implements ValueObject { @@ -20,25 +20,26 @@ public static FolderName of(final String value) { } @Override - public void validate(ValidationHandler aHandler) { + public void validate(ValidationHandler handler) { if (value == null) { - aHandler.append(new Error("'name' cannot be null.")); + handler.append(new ValidationError("'name' cannot be null.")); return; } if (value.trim().isEmpty()) { - aHandler.append(new Error("'name' cannot be empty.")); + handler.append(new ValidationError("'name' cannot be empty.")); return; } String trimmedName = value.trim(); if (trimmedName.length() < MIN_LENGTH || trimmedName.length() > MAX_LENGTH) - aHandler.append( - new Error("'name' must be between " + MIN_LENGTH + " and " + MAX_LENGTH + " characters.")); + handler.append( + new ValidationError( + "'name' must be between " + MIN_LENGTH + " and " + MAX_LENGTH + " characters.")); if (containsInvalidCharacters(value)) - aHandler.append(new Error("'name' contains invalid characters.")); + handler.append(new ValidationError("'name' contains invalid characters.")); } diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/FolderValidator.java b/domain/src/main/java/com/callv2/drive/domain/folder/FolderValidator.java index ee9b570b..c7b4f0c7 100644 --- a/domain/src/main/java/com/callv2/drive/domain/folder/FolderValidator.java +++ b/domain/src/main/java/com/callv2/drive/domain/folder/FolderValidator.java @@ -7,9 +7,9 @@ public class FolderValidator extends Validator { private final Folder folder; - protected FolderValidator(final Folder aFolder, final ValidationHandler aHandler) { - super(aHandler); - this.folder = aFolder; + protected FolderValidator(final Folder folder, final ValidationHandler handler) { + super(handler); + this.folder = folder; } @Override diff --git a/domain/src/main/java/com/callv2/drive/domain/folder/SubFolder.java b/domain/src/main/java/com/callv2/drive/domain/folder/SubFolder.java deleted file mode 100644 index 0fa935fc..00000000 --- a/domain/src/main/java/com/callv2/drive/domain/folder/SubFolder.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.callv2.drive.domain.folder; - -import com.callv2.drive.domain.ValueObject; - -public record SubFolder(FolderID id, FolderName name) implements ValueObject { - - public static SubFolder with(final FolderID id, final FolderName name) { - return new SubFolder(id, name); - } - - public static SubFolder from(final Folder folder) { - return new SubFolder(folder.getId(), folder.getName()); - } - -} diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Member.java b/domain/src/main/java/com/callv2/drive/domain/member/Member.java index dec93581..fd8529cd 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Member.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Member.java @@ -4,46 +4,69 @@ import java.util.Optional; import com.callv2.drive.domain.AggregateRoot; +import com.callv2.drive.domain.exception.IdMismatchException; +import com.callv2.drive.domain.exception.SynchronizedVersionOutdatedException; import com.callv2.drive.domain.exception.ValidationException; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.handler.Notification; public class Member extends AggregateRoot { + private Username username; + private Nickname nickname; + private Quota quota; private Optional quotaRequest; + private boolean hasSystemAccess; + private Instant createdAt; private Instant updatedAt; + private Long synchronizedVersion; + private Member( final MemberID id, + final Username username, + final Nickname nickname, final Quota quota, final QuotaRequest quotaRequest, + final boolean hasSystemAccess, final Instant createdAt, - final Instant updatedAt) { + final Instant updatedAt, + final Long version) { super(id); - this.quota = quota; + this.username = username; + this.nickname = nickname; + this.quota = quota == null ? Quota.of(0, QuotaUnit.BYTE) : quota; this.quotaRequest = Optional.ofNullable(quotaRequest); + this.hasSystemAccess = hasSystemAccess; this.createdAt = createdAt; this.updatedAt = updatedAt; - } - public static Member create(final MemberID id) { - - final Instant now = Instant.now(); - final Quota quota = Quota.of(0, QuotaUnit.BYTE); - - return new Member(id, quota, null, now, now); + this.synchronizedVersion = version == null ? 0L : version; } public static Member with( final MemberID id, + final Username username, + final Nickname nickname, final Quota quota, final QuotaRequest quotaRequest, + final boolean hasSystemAccess, final Instant createdAt, - final Instant updatedAt) { - return new Member(id, quota, quotaRequest, createdAt, updatedAt); + final Instant updatedAt, + final Long synchronizedVersion) { + return new Member( + id, + username, + nickname, + quota, + quotaRequest, + hasSystemAccess, + createdAt, + updatedAt, + synchronizedVersion); } @Override @@ -51,23 +74,41 @@ public void validate(final ValidationHandler handler) { new MemberValidator(this, handler).validate(); } - public Member requestQuota(final Quota quota) { + public Member synchronize(final Member member) { - if (quota == null) - return this; + if (!this.id.equals(member.id)) + throw IdMismatchException.with( + Member.class, + member.id.getValue()); + + if (this.synchronizedVersion > member.synchronizedVersion) + throw SynchronizedVersionOutdatedException + .with( + Member.class, + this.synchronizedVersion, + member.synchronizedVersion); + + this.nickname = member.nickname; + this.username = member.username; + this.hasSystemAccess = member.hasSystemAccess; + this.createdAt = member.createdAt; + this.updatedAt = member.updatedAt; + this.synchronizedVersion = member.synchronizedVersion; + + return this; + } + + public Member requestQuota(final Quota quota) { - final QuotaRequest actualQuotaRequest = this.quotaRequest.orElse(null); final QuotaRequest newQuotaRequest = QuotaRequest.of(quota, Instant.now()); - this.quotaRequest = Optional.ofNullable(newQuotaRequest); final Notification notification = Notification.create(); - this.validate(notification); + newQuotaRequest.validate(notification); - if (notification.hasError()) { - this.quotaRequest = Optional.ofNullable(actualQuotaRequest); + if (notification.hasError()) throw ValidationException.with("Request Quota Error", notification); - } + this.quotaRequest = Optional.ofNullable(newQuotaRequest); this.updatedAt = Instant.now(); return this; } @@ -99,6 +140,14 @@ public Member reproveQuotaRequest() { return this; } + public Username getUsername() { + return username; + } + + public Nickname getNickname() { + return nickname; + } + public Quota getQuota() { return quota; } @@ -107,6 +156,10 @@ public Optional getQuotaRequest() { return quotaRequest; } + public boolean hasSystemAccess() { + return hasSystemAccess; + } + public Instant getCreatedAt() { return createdAt; } @@ -115,4 +168,15 @@ public Instant getUpdatedAt() { return updatedAt; } + public Long getSynchronizedVersion() { + return synchronizedVersion; + } + + @Override + public String toString() { + return "Member [id=" + id + ", username=" + username + ", nickname=" + nickname + ", quota=" + quota + + ", quotaRequest=" + quotaRequest + ", hasSystemAccess=" + hasSystemAccess + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + ", synchronizedVersion=" + synchronizedVersion + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/MemberID.java b/domain/src/main/java/com/callv2/drive/domain/member/MemberID.java index 9310602a..2be9ec43 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/MemberID.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/MemberID.java @@ -12,4 +12,9 @@ public static MemberID of(final String id) { return new MemberID(id); } + @Override + public String toString() { + return "MemberID [value=" + getValue() + "]"; + } + } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java b/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java index 0558a0f6..80ef73b1 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/MemberValidator.java @@ -1,6 +1,6 @@ package com.callv2.drive.domain.member; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; import com.callv2.drive.domain.validation.Validator; @@ -8,30 +8,50 @@ public class MemberValidator extends Validator { private final Member member; - protected MemberValidator(final Member aMember, final ValidationHandler aHandler) { - super(aHandler); + protected MemberValidator(final Member aMember, final ValidationHandler handler) { + super(handler); this.member = aMember; } @Override public void validate() { validateId(); + validateUsername(); + validateNickname(); validateQuota(); validateQuotaRequest(); } private void validateId() { if (this.member.getId() == null) { - this.validationHandler().append(Error.with("'id' is required")); + this.validationHandler().append(ValidationError.with("'id' is required")); return; } this.member.getId().validate(this.validationHandler()); } + private void validateUsername() { + if (this.member.getUsername() == null) { + this.validationHandler().append(ValidationError.with("'username' is required")); + return; + } + + this.member.getUsername().validate(this.validationHandler()); + } + + private void validateNickname() { + if (this.member.getNickname() == null) { + this.validationHandler().append(ValidationError.with("'nickname' is required")); + return; + } + + this.member.getNickname().validate(this.validationHandler()); + } + private void validateQuota() { if (this.member.getQuota() == null) { - this.validationHandler().append(Error.with("'quota' is required")); + this.validationHandler().append(ValidationError.with("'quota' is required")); return; } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java b/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java new file mode 100644 index 00000000..4a31f9ce --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/member/Nickname.java @@ -0,0 +1,19 @@ +package com.callv2.drive.domain.member; + +import com.callv2.drive.domain.ValueObject; +import com.callv2.drive.domain.validation.ValidationError; +import com.callv2.drive.domain.validation.ValidationHandler; + +public record Nickname(String value) implements ValueObject { + + public static Nickname of(final String nickname) { + return new Nickname(nickname); + } + + @Override + public void validate(final ValidationHandler handler) { + if (value == null || value.isBlank()) + handler.append(ValidationError.with("'nickname' is required")); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Quota.java b/domain/src/main/java/com/callv2/drive/domain/member/Quota.java index f2334f4f..0a5ed1de 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/Quota.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/Quota.java @@ -2,7 +2,7 @@ import java.util.Objects; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.ValueObject; import com.callv2.drive.domain.validation.ValidationHandler; @@ -17,15 +17,15 @@ public long sizeInBytes() { } @Override - public void validate(ValidationHandler aHandler) { + public void validate(ValidationHandler handler) { if (Objects.isNull(this.amount)) - aHandler.append(new Error("'amount' should not be null")); + handler.append(new ValidationError("'amount' should not be null")); if (!Objects.isNull(this.amount) && this.amount < 0) - aHandler.append(new Error("'amount' should be greater than 0")); + handler.append(new ValidationError("'amount' should be greater than 0")); if (Objects.isNull(this.unit)) - aHandler.append(new Error("'unit' should not be null")); + handler.append(new ValidationError("'unit' should not be null")); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequest.java b/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequest.java index 9adec269..74070279 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequest.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequest.java @@ -3,7 +3,7 @@ import java.time.Instant; import com.callv2.drive.domain.ValueObject; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public record QuotaRequest(Quota quota, Instant requesteddAt) implements ValueObject { @@ -15,10 +15,10 @@ public static QuotaRequest of(Quota quota, Instant requestedAt) { @Override public void validate(ValidationHandler handler) { if (quota == null) - handler.append(new Error("'quota' should not be null")); + handler.append(new ValidationError("'quota' should not be null")); if (requesteddAt == null) - handler.append(new Error("'requestedAt' should not be null")); + handler.append(new ValidationError("'requestedAt' should not be null")); if (quota != null) quota.validate(handler); diff --git a/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequestPreview.java b/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequestPreview.java index 8cc8fc45..be178c5f 100644 --- a/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequestPreview.java +++ b/domain/src/main/java/com/callv2/drive/domain/member/QuotaRequestPreview.java @@ -4,8 +4,10 @@ public record QuotaRequestPreview( String memberId, - long amount, - QuotaUnit unit, - Instant requestedAt) { + String memberUsername, + String memberNickname, + long quotaAmount, + QuotaUnit quotaUnit, + Instant quotaRequestedAt) { } diff --git a/domain/src/main/java/com/callv2/drive/domain/member/Username.java b/domain/src/main/java/com/callv2/drive/domain/member/Username.java new file mode 100644 index 00000000..913b6d5f --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/member/Username.java @@ -0,0 +1,22 @@ +package com.callv2.drive.domain.member; + +import com.callv2.drive.domain.ValueObject; +import com.callv2.drive.domain.validation.ValidationError; +import com.callv2.drive.domain.validation.ValidationHandler; + +public record Username(String value) implements ValueObject { + + public static Username of(final String username) { + return new Username(username); + } + + @Override + public void validate(final ValidationHandler handler) { + if (value == null || value.isBlank()) + handler.append(ValidationError.with("'username' is required")); + + if (value.contains(" ")) + handler.append(ValidationError.with("'username' cannot contain spaces")); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/pagination/Filter.java b/domain/src/main/java/com/callv2/drive/domain/pagination/Filter.java index eff893b5..71314b0e 100644 --- a/domain/src/main/java/com/callv2/drive/domain/pagination/Filter.java +++ b/domain/src/main/java/com/callv2/drive/domain/pagination/Filter.java @@ -4,7 +4,7 @@ import java.util.Optional; import com.callv2.drive.domain.exception.ValidationException; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.handler.Notification; public record Filter(String field, String value, String valueToCompare, Type type) { @@ -13,17 +13,17 @@ public record Filter(String field, String value, String valueToCompare, Type typ final Notification notification = Notification.create(); if (field == null) - notification.append(Error.with("Filter.field cannot be null")); + notification.append(ValidationError.with("Filter.field cannot be null")); if (field != null && field.isBlank()) - notification.append(Error.with("Filter.field cannot be blank")); + notification.append(ValidationError.with("Filter.field cannot be blank")); if (value == null) - notification.append(Error.with("Filter.value cannot be null")); + notification.append(ValidationError.with("Filter.value cannot be null")); if (type == null) notification.append( - Error.with( + ValidationError.with( "Filter.type cannot be null, Valid values are: " + Arrays.toString(Type.values()))); if (notification.hasError()) diff --git a/domain/src/main/java/com/callv2/drive/domain/pagination/Page.java b/domain/src/main/java/com/callv2/drive/domain/pagination/Page.java index 0a26ea3b..55fb5841 100644 --- a/domain/src/main/java/com/callv2/drive/domain/pagination/Page.java +++ b/domain/src/main/java/com/callv2/drive/domain/pagination/Page.java @@ -11,10 +11,10 @@ public record Page( List items) { public Page map(final Function mapper) { - final List aNewList = this.items.stream() + final List newList = this.items.stream() .map(mapper) .toList(); - return new Page<>(currentPage(), perPage(), totalPages(), total(), aNewList); + return new Page<>(currentPage(), perPage(), totalPages(), total(), newList); } } diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/Error.java b/domain/src/main/java/com/callv2/drive/domain/validation/Error.java deleted file mode 100644 index 7d649f01..00000000 --- a/domain/src/main/java/com/callv2/drive/domain/validation/Error.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.callv2.drive.domain.validation; - -public record Error(String message) { - - public static Error with(final String message) { - return new Error(message); - } - -} diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/ValidationError.java b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationError.java new file mode 100644 index 00000000..dcba3d38 --- /dev/null +++ b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationError.java @@ -0,0 +1,19 @@ +package com.callv2.drive.domain.validation; + +import com.callv2.drive.domain.exception.DomainException; + +public record ValidationError(String message) { + + public static ValidationError with(final String message) { + return new ValidationError(message); + } + + public static ValidationError fromDomainError(final DomainException.Error domainError) { + return new ValidationError(domainError.message()); + } + + public DomainException.Error toDomainError() { + return DomainException.Error.with(message()); + } + +} diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java index 2c6f5afc..27a7965a 100644 --- a/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java +++ b/domain/src/main/java/com/callv2/drive/domain/validation/ValidationHandler.java @@ -4,13 +4,13 @@ public interface ValidationHandler { - ValidationHandler append(final Error anError); + ValidationHandler append(final ValidationError error); - ValidationHandler append(final ValidationHandler anHandler); + ValidationHandler append(final ValidationHandler handler); - T valdiate(final Validation aValidation); + T validate(final Validation validation); - List getErrors(); + List getErrors(); default boolean hasError() { return getErrors() != null && !getErrors().isEmpty(); diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/Validator.java b/domain/src/main/java/com/callv2/drive/domain/validation/Validator.java index 301722c9..4f891443 100644 --- a/domain/src/main/java/com/callv2/drive/domain/validation/Validator.java +++ b/domain/src/main/java/com/callv2/drive/domain/validation/Validator.java @@ -4,8 +4,8 @@ public abstract class Validator { private final ValidationHandler handler; - protected Validator(final ValidationHandler aHandler) { - this.handler = aHandler; + protected Validator(final ValidationHandler handler) { + this.handler = handler; } public abstract void validate(); diff --git a/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java b/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java index 45abd598..14cca621 100644 --- a/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java +++ b/domain/src/main/java/com/callv2/drive/domain/validation/handler/Notification.java @@ -4,57 +4,53 @@ import java.util.List; import com.callv2.drive.domain.exception.DomainException; -import com.callv2.drive.domain.validation.Error; +import com.callv2.drive.domain.validation.ValidationError; import com.callv2.drive.domain.validation.ValidationHandler; public class Notification implements ValidationHandler { - private final List errors; + private final List errors; - private Notification(final List errors) { - this.errors = errors; + private Notification(final List errors) { + this.errors = errors == null ? new ArrayList<>() : errors; } public static Notification create() { return new Notification(new ArrayList<>()); } - public static Notification create(final Throwable t) { - return new Notification(new ArrayList<>()).append(new Error(t.getMessage())); - } - - public static Notification create(final Error anError) { - return new Notification(new ArrayList<>()).append(anError); + public static Notification create(final ValidationError error) { + return new Notification(new ArrayList<>()).append(error); } @Override - public Notification append(final Error anError) { - this.errors.add(anError); + public Notification append(final ValidationError error) { + this.errors.add(error); return this; } @Override - public Notification append(final ValidationHandler anHandler) { - this.errors.addAll(anHandler.getErrors()); + public Notification append(final ValidationHandler handler) { + this.errors.addAll(handler.getErrors()); return this; } @Override - public T valdiate(final Validation aValidation) { + public T validate(final Validation validation) { try { - return aValidation.validate(); + return validation.validate(); } catch (DomainException e) { - this.errors.addAll(e.getErrors()); + this.errors.addAll(e.getErrors().stream().map(ValidationError::fromDomainError).toList()); } catch (Throwable e) { - this.errors.add(new Error(e.getMessage())); + this.errors.add(new ValidationError(e.getMessage())); } return null; } @Override - public List getErrors() { - return this.errors; + public List getErrors() { + return this.errors == null ? List.of() : List.copyOf(this.errors); } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 8f20ed0b..00000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,12 +0,0 @@ -# This file was generated by the Gradle 'init' task. -# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format - -[versions] -commons-math3 = "3.6.1" -guava = "33.0.0-jre" -junit-jupiter = "5.10.2" - -[libraries] -commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } -guava = { module = "com.google.guava:guava", version.ref = "guava" } -junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } diff --git a/infrastructure/build.gradle b/infrastructure/build.gradle index 4305c30a..222aa944 100644 --- a/infrastructure/build.gradle +++ b/infrastructure/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' id 'application' - id 'org.springframework.boot' version '3.4.5' + id 'org.springframework.boot' version '3.5.5' id 'io.spring.dependency-management' version '1.1.7' } @@ -20,31 +20,28 @@ dependencies { implementation(project(":domain")) implementation(project(":application")) + implementation(project(":aop")) implementation 'org.springframework.boot:spring-boot-starter-web' + + implementation 'org.springframework.boot:spring-boot-starter-amqp' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.postgresql:postgresql:42.7.5' + implementation 'org.postgresql:postgresql:42.7.7' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.11' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.19.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' - testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' + testImplementation 'org.mockito:mockito-junit-jupiter:5.19.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - // testImplementation 'com.h2database:h2:2.3.232' - implementation 'com.h2database:h2:2.3.232' - - implementation libs.guava - - implementation 'org.springframework:spring-aop' implementation 'org.springframework.boot:spring-boot-starter-log4j2' } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java new file mode 100644 index 00000000..4952cc9a --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/ApplicationLayerAspect.java @@ -0,0 +1,35 @@ +package com.callv2.drive.infrastructure.aop.aspect; + +import org.apache.logging.log4j.Level; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; +import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; +import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodResultPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; + +@Aspect +@Component +public class ApplicationLayerAspect { + + private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder + .create() + .preExecutor(LogMethodSignaturePreExecutor.create(Level.INFO)) + .preExecutor(LogMethodArgsPreExecutor.create(Level.DEBUG)) + .postExecutor(LogMethodResultPostExecutor.create(Level.DEBUG)) + .postExecutor(LogTelemetryPostExecutor.create(Level.INFO)) + .errorExecutor(LogErrorPostExecutor.create(Level.ERROR)) + .build(); + + @Around("execution(* com.callv2.drive.application..*.*(..))") + public Object aspect(final ProceedingJoinPoint joinPoint) throws Throwable { + return chainHandler.handle(joinPoint); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java new file mode 100644 index 00000000..9a53f1b5 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/DomainLayerAspect.java @@ -0,0 +1,35 @@ +package com.callv2.drive.infrastructure.aop.aspect; + +import org.apache.logging.log4j.Level; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; +import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; +import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodResultPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; + +@Aspect +@Component +public class DomainLayerAspect { + + private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder + .create() + .preExecutor(LogMethodSignaturePreExecutor.create(Level.INFO)) + .preExecutor(LogMethodArgsPreExecutor.create(Level.DEBUG)) + .postExecutor(LogMethodResultPostExecutor.create(Level.DEBUG)) + .postExecutor(LogTelemetryPostExecutor.create(Level.INFO)) + .errorExecutor(LogErrorPostExecutor.create(Level.ERROR)) + .build(); + + @Around("execution(* com.callv2.drive.domain..*.*(..))") + public Object aspect(final ProceedingJoinPoint joinPoint) throws Throwable { + return chainHandler.handle(joinPoint); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java new file mode 100644 index 00000000..8cfce871 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspect/InfrastructureLayerAspect.java @@ -0,0 +1,40 @@ +package com.callv2.drive.infrastructure.aop.aspect; + +import org.apache.logging.log4j.Level; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import com.callv2.aop.executor.chain.handler.ExecutorChainHandler; +import com.callv2.aop.executor.chain.handler.SimpleExecutorChainHandlerBuilder; +import com.callv2.drive.infrastructure.aop.executor.LogErrorPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodArgsPreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodResultPostExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogMethodSignaturePreExecutor; +import com.callv2.drive.infrastructure.aop.executor.LogTelemetryPostExecutor; + +@Aspect +@Component +public class InfrastructureLayerAspect { + + private final ExecutorChainHandler chainHandler = SimpleExecutorChainHandlerBuilder + .create() + .preExecutor(LogMethodSignaturePreExecutor.create(Level.INFO)) + .preExecutor(LogMethodArgsPreExecutor.create(Level.DEBUG)) + .postExecutor(LogMethodResultPostExecutor.create(Level.DEBUG)) + .postExecutor(LogTelemetryPostExecutor.create(Level.INFO)) + .errorExecutor(LogErrorPostExecutor.create(Level.ERROR)) + .build(); + + @Around("execution(* com.callv2.drive.infrastructure.api.controller..*.*(..))") + public Object controllerAspect(final ProceedingJoinPoint joinPoint) throws Throwable { + return chainHandler.handle(joinPoint); + } + + @Around("execution(* com.callv2.drive.infrastructure.messaging..*.*(..))") + public Object messagingAspect(final ProceedingJoinPoint joinPoint) throws Throwable { + return chainHandler.handle(joinPoint); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/AspectExecutorChain.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/AspectExecutorChain.java deleted file mode 100644 index b7aa2c67..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/AspectExecutorChain.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.chain; - -import java.util.ArrayDeque; -import java.util.Queue; - -import org.aopalliance.intercept.Joinpoint; - -import com.callv2.drive.infrastructure.aop.aspects.executor.AspectExecutor; - -public abstract class AspectExecutorChain> { - - private AspectExecutorChain next; - private final E executor; - - protected AspectExecutorChain(final E executor) { - this.executor = executor; - } - - public final O execute(final J joinpoint) throws Throwable { - - executor.execute(joinpoint); - - if (next != null) - return next.execute(joinpoint); - - return callsProceed(joinpoint); - } - - protected abstract O callsProceed(J joinpoint) throws Throwable; - - @SuppressWarnings("unchecked") - protected AspectExecutorChain setNext(final AspectExecutorChain next) { - return this.next = (AspectExecutorChain) next; - } - - public static final class Builder> { - - private final Class clazz; - private final Queue chains; - - private Builder(final Class clazz) { - this.clazz = clazz; - this.chains = new ArrayDeque<>(); - } - - public static > Builder create(final Class clazz) { - return new Builder(clazz); - } - - public Builder add(final C chain) { - - if (chain.getClass() != clazz) - throw new IllegalArgumentException("Chain must be exactly of type " + clazz.getName()); - - this.chains.add(chain); - return this; - } - - @SuppressWarnings("unchecked") - public C build() { - if (chains.isEmpty()) - return null; - - final var firstChain = chains.poll(); - var chain = firstChain; - - do { - chain = (C) chain.setNext(chains.poll()); - } while (!chains.isEmpty()); - - return firstChain; - } - - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/MethodInvocationAspectExecutorChain.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/MethodInvocationAspectExecutorChain.java deleted file mode 100644 index 2f0e954d..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/MethodInvocationAspectExecutorChain.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.chain; - -import com.callv2.drive.infrastructure.aop.aspects.context.MethodInvocationContext; -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; -import com.callv2.drive.infrastructure.aop.aspects.executor.AspectExecutor; - -public final class MethodInvocationAspectExecutorChain extends - AspectExecutorChain> { - - private MethodInvocationAspectExecutorChain(final AspectExecutor executor) { - super(executor); - } - - public static MethodInvocationAspectExecutorChain with(final AspectExecutor executor) { - return new MethodInvocationAspectExecutorChain(executor); - } - - @Override - protected PostInvocationContext callsProceed(final MethodInvocationContext joinpoint) throws Throwable { - return joinpoint.proceedWithContext(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/PostInvocationAspectExecutorChain.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/PostInvocationAspectExecutorChain.java deleted file mode 100644 index 8f4a953b..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/PostInvocationAspectExecutorChain.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.chain; - -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; -import com.callv2.drive.infrastructure.aop.aspects.executor.AspectExecutor; - -public final class PostInvocationAspectExecutorChain extends - AspectExecutorChain> { - - private PostInvocationAspectExecutorChain(final AspectExecutor executor) { - super(executor); - } - - public static PostInvocationAspectExecutorChain with(final AspectExecutor executor) { - return new PostInvocationAspectExecutorChain(executor); - } - - @Override - protected Object callsProceed(final PostInvocationContext joinpoint) throws Throwable { - if (joinpoint.wasSuccessful()) - return joinpoint.getResult(); - else - throw joinpoint.getThrowable(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/Proceeder.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/Proceeder.java deleted file mode 100644 index 1dad486c..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/chain/Proceeder.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.chain; - -import org.aopalliance.intercept.Joinpoint; - -@FunctionalInterface -public interface Proceeder { - - O proceed(J joinpoint) throws Throwable; - -} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java deleted file mode 100644 index 1ba24ab1..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractMethodInvocationContext.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.time.Instant; - -import 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 deleted file mode 100644 index bdeae37f..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/AbstractPostInvocationContext.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.time.Instant; -import java.util.concurrent.atomic.AtomicBoolean; - -public abstract class AbstractPostInvocationContext implements PostInvocationContext { - - private final Object result; - private final Throwable throwable; - private final Instant proceededAt; - private final AtomicBoolean successful; - - protected AbstractPostInvocationContext( - final Object result, - final Throwable throwable, - final Instant proceededAt, - final boolean successful) { - this.result = result; - this.throwable = throwable; - this.proceededAt = proceededAt; - this.successful = new AtomicBoolean(successful); - } - - @Override - public Instant getProceededAt() { - return proceededAt; - } - - @Override - public Object getResult() { - return result; - } - - @Override - public Throwable getThrowable() { - return throwable; - } - - @Override - public boolean wasSuccessful() { - return successful.get(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java deleted file mode 100644 index 151fb732..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/MethodInvocationContext.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.time.Instant; - -import 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 deleted file mode 100644 index 41a206b8..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/PostInvocationContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.time.Instant; - -import 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 deleted file mode 100644 index b5e5d4ea..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimpleMethodInvocationContext.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.lang.reflect.AccessibleObject; -import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.aopalliance.intercept.MethodInvocation; - -public final class SimpleMethodInvocationContext extends AbstractMethodInvocationContext { - - private final AtomicBoolean proceeded; - private final MethodInvocation methodInvocation; - - private SimpleMethodInvocationContext(final MethodInvocation methodInvocation) { - super(); - this.proceeded = new AtomicBoolean(false); - this.methodInvocation = methodInvocation; - } - - public static SimpleMethodInvocationContext of(final MethodInvocation methodInvocation) { - return new SimpleMethodInvocationContext(methodInvocation); - } - - @Override - public boolean proceeded() { - return proceeded.get(); - } - - @Override - @Nonnull - public Method getMethod() { - return methodInvocation.getMethod(); - } - - @Override - @Nonnull - public Object[] getArguments() { - return methodInvocation.getArguments(); - } - - @Override - @Nullable - public Object proceed() throws Throwable { - if (proceeded.getAndSet(true)) - throw new IllegalStateException("Method already proceeded"); - return methodInvocation.proceed(); - } - - @Override - @Nullable - public Object getThis() { - return methodInvocation.getThis(); - } - - @Override - @Nonnull - public AccessibleObject getStaticPart() { - return methodInvocation.getStaticPart(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimplePostInvocationContext.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimplePostInvocationContext.java deleted file mode 100644 index 30b55dfc..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/context/SimplePostInvocationContext.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.context; - -import java.lang.reflect.AccessibleObject; -import java.lang.reflect.Method; -import java.time.Instant; -import java.util.Objects; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public final class SimplePostInvocationContext extends AbstractPostInvocationContext { - - private final MethodInvocationContext methodInvocationContext; - - private SimplePostInvocationContext( - final Object result, - final Throwable throwable, - final Instant proceededAt, - final boolean successful, - final MethodInvocationContext methodInvocationContext) { - super(result, throwable, proceededAt, successful); - this.methodInvocationContext = Objects.requireNonNull(methodInvocationContext, - "'methodInvocationContext' must not be null"); - } - - public static final PostInvocationContext captureFromExecution( - final MethodInvocationContext methodInvocationContext) { - - Objects.requireNonNull(methodInvocationContext, "'methodInvocationContext' must not be null"); - - Object result; - Throwable throwable; - boolean successful; - final Instant proceededAt; - - try { - result = methodInvocationContext.proceed(); - throwable = null; - successful = true; - } catch (Throwable e) { - result = null; - throwable = e; - successful = false; - } finally { - proceededAt = Instant.now(); - } - - return new SimplePostInvocationContext( - result, - throwable, - proceededAt, - successful, - methodInvocationContext); - - } - - @Override - @Nonnull - public Instant getContextedAt() { - return methodInvocationContext.getContextedAt(); - } - - @Override - public boolean proceeded() { - return methodInvocationContext.proceeded(); - } - - @Override - @Nonnull - public PostInvocationContext proceedWithContext() { - return this; - } - - @Override - @Nonnull - public Method getMethod() { - return methodInvocationContext.getMethod(); - } - - @Override - @Nonnull - public Object[] getArguments() { - return methodInvocationContext.getArguments(); - } - - @Override - @Nullable - public Object proceed() throws Throwable { - return methodInvocationContext.proceed(); - } - - @Override - @Nullable - public Object getThis() { - return methodInvocationContext.getThis(); - } - - @Override - @Nonnull - public AccessibleObject getStaticPart() { - return methodInvocationContext.getStaticPart(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ArgsLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ArgsLogExecutor.java deleted file mode 100644 index 8519ed3e..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ArgsLogExecutor.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import java.util.Arrays; - -import org.apache.logging.log4j.Level; - -import com.callv2.drive.infrastructure.aop.aspects.context.MethodInvocationContext; - -public class ArgsLogExecutor extends Log4jExecutor { - - public ArgsLogExecutor(final Level level, final Class clazz) { - super(level, clazz); - } - - @Override - public void execute(final MethodInvocationContext context) { - final var args = context.getArguments(); - log("<> [{}] <> count:[{}] args: [{}]", - context.getMethod(), - args.length, - Arrays.toString(args)); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/AspectExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/AspectExecutor.java deleted file mode 100644 index 9b7cf5da..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/AspectExecutor.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import org.aopalliance.intercept.Joinpoint; - -@FunctionalInterface -public interface AspectExecutor { - - void execute(J joinPoint); - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/IdentifiableAspectExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/IdentifiableAspectExecutor.java deleted file mode 100644 index 8a96e709..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/IdentifiableAspectExecutor.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import org.aopalliance.intercept.Joinpoint; - -public interface IdentifiableAspectExecutor extends AspectExecutor { - - default String getId() { - return getClass().getSimpleName(); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/MethodSignatureLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/MethodSignatureLogExecutor.java deleted file mode 100644 index 4c8ec16b..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/MethodSignatureLogExecutor.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import org.apache.logging.log4j.Level; - -import com.callv2.drive.infrastructure.aop.aspects.context.MethodInvocationContext; - -public class MethodSignatureLogExecutor extends Log4jExecutor { - - public MethodSignatureLogExecutor(final Level level, final Class clazz) { - super(level, clazz); - } - - @Override - public void execute(final MethodInvocationContext context) { - log("<>: [{}]", context.getMethod().toString()); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/PostTelemetryLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/PostTelemetryLogExecutor.java deleted file mode 100644 index 48675064..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/PostTelemetryLogExecutor.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import java.time.Duration; - -import org.apache.logging.log4j.Level; - -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; - -public class PostTelemetryLogExecutor extends Log4jExecutor { - - public PostTelemetryLogExecutor(final Level logLevel, final Class clazz) { - super(logLevel, clazz); - } - - @Override - public void execute(final PostInvocationContext context) { - log("<> [{}] ms <> [{}]", - Duration.between(context.getContextedAt(), context.getProceededAt()).toMillis(), - context.getMethod().toString()); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java deleted file mode 100644 index 6bef77d9..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/ThrowableLogExecutor.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; - -import com.callv2.drive.infrastructure.aop.aspects.context.PostInvocationContext; -import org.apache.logging.log4j.Level; - -public class ThrowableLogExecutor extends Log4jExecutor { - - private ThrowableLogExecutor(final Level level, final Class clazz) { - super(level, clazz); - } - - public static ThrowableLogExecutor defaultCreate(final Class clazz) { - return new ThrowableLogExecutor(Level.ERROR, clazz); - } - - public static ThrowableLogExecutor create(final Level level, final Class clazz) { - return new ThrowableLogExecutor(level, clazz); - } - - @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 deleted file mode 100644 index 4ae41e75..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/handler/SimpleMethodInterceptorWithContextHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -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/aop/aspects/executor/Log4jExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java similarity index 79% rename from infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/Log4jExecutor.java rename to infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java index 16195a1f..b4303d38 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/aspects/executor/Log4jExecutor.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/Log4jLogger.java @@ -1,18 +1,16 @@ -package com.callv2.drive.infrastructure.aop.aspects.executor; +package com.callv2.drive.infrastructure.aop.executor; -import org.aopalliance.intercept.Joinpoint; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.Message; -public abstract class Log4jExecutor implements IdentifiableAspectExecutor { +public abstract class Log4jLogger { private final Logger logger; private final Level logLevel; - public Log4jExecutor(final Level logLevel, Class clazz) { - super(); + protected Log4jLogger(final Level logLevel, final Class clazz) { this.logLevel = logLevel; this.logger = LogManager.getLogger(clazz); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java new file mode 100644 index 00000000..943d3258 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogErrorPostExecutor.java @@ -0,0 +1,30 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.executor.PostExecutor; + +public class LogErrorPostExecutor extends Log4jLogger implements PostExecutor { + + private LogErrorPostExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); + } + + public static LogErrorPostExecutor create(final Level logLevel) { + return new LogErrorPostExecutor(logLevel, LogErrorPostExecutor.class); + } + + @Override + public void execute(final PostInvocationContext joinPoint) { + if (joinPoint.getThrowable() != null) + log("<> [{}] <> [{}] <> [{}]", + joinPoint.getSignature().toShortString(), + joinPoint.getThrowable().getClass().getName(), + joinPoint.getThrowable().getMessage(), + joinPoint.getThrowable()); + else + log("<> [{}] <>", joinPoint.getSignature().toShortString()); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java new file mode 100644 index 00000000..705dbf1b --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodArgsPreExecutor.java @@ -0,0 +1,28 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import java.util.Arrays; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.PreExecutor; + +public class LogMethodArgsPreExecutor extends Log4jLogger implements PreExecutor { + + private LogMethodArgsPreExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); + } + + public static LogMethodArgsPreExecutor create(final Level logLevel) { + return new LogMethodArgsPreExecutor(logLevel, LogMethodArgsPreExecutor.class); + } + + @Override + public void execute(final PreInvocationContext joinPoint) { + + log("<> [{}] <> [{}]", + joinPoint.getSignature().toShortString(), + Arrays.toString(joinPoint.getArgs())); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java new file mode 100644 index 00000000..99ba0811 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodResultPostExecutor.java @@ -0,0 +1,29 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import java.util.Arrays; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.executor.PostExecutor; + +public class LogMethodResultPostExecutor extends Log4jLogger implements PostExecutor { + + private LogMethodResultPostExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); + } + + public static LogMethodResultPostExecutor create(final Level logLevel) { + return new LogMethodResultPostExecutor(logLevel, LogMethodResultPostExecutor.class); + } + + @Override + public void execute(final PostInvocationContext joinPoint) { + + log("<> [{}] <> [{}] <> [{}]", + joinPoint.getSignature().toShortString(), + Arrays.toString(joinPoint.getArgs()), + joinPoint.getResult()); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java new file mode 100644 index 00000000..90fef616 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogMethodSignaturePreExecutor.java @@ -0,0 +1,23 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PreInvocationContext; +import com.callv2.aop.executor.PreExecutor; + +public class LogMethodSignaturePreExecutor extends Log4jLogger implements PreExecutor { + + private LogMethodSignaturePreExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); + } + + public static LogMethodSignaturePreExecutor create(final Level logLevel) { + return new LogMethodSignaturePreExecutor(logLevel, LogMethodSignaturePreExecutor.class); + } + + @Override + public void execute(final PreInvocationContext joinPoint) { + log("<>: [{}]", joinPoint.getSignature().toShortString()); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java new file mode 100644 index 00000000..1bc4769b --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/aop/executor/LogTelemetryPostExecutor.java @@ -0,0 +1,27 @@ +package com.callv2.drive.infrastructure.aop.executor; + +import java.time.Duration; + +import org.apache.logging.log4j.Level; + +import com.callv2.aop.context.PostInvocationContext; +import com.callv2.aop.executor.PostExecutor; + +public class LogTelemetryPostExecutor extends Log4jLogger implements PostExecutor { + + private LogTelemetryPostExecutor(final Level logLevel, final Class clazz) { + super(logLevel, clazz); + } + + public static LogTelemetryPostExecutor create(final Level logLevel) { + return new LogTelemetryPostExecutor(logLevel, LogTelemetryPostExecutor.class); + } + + @Override + public void execute(final PostInvocationContext joinPoint) { + log("<>: [{}] <> [{}] ms", + joinPoint.getSignature().toShortString(), + Duration.between(joinPoint.getContextedAt(), joinPoint.getProceededAt()).toMillis()); + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java index bd0cd9ab..9f61d40f 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FileAPI.java @@ -6,12 +6,12 @@ import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; import com.callv2.drive.domain.pagination.Filter; @@ -26,7 +26,6 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -34,50 +33,48 @@ @RequestMapping("files") public interface FileAPI { - @PostMapping(value = "/folders/{folderId}/upload", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE }, produces = { - MediaType.APPLICATION_JSON_VALUE }) - @Operation(summary = "Upload a file to a specific folder", description = "This method uploads a file", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "File uploaded successfully", content = @Content(schema = @Schema(implementation = CreateFileResponse.class))), - @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "413", description = "File is too large", content = @Content(schema = @Schema(implementation = ApiError.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity create( - @PathVariable(required = true, name = "folderId") UUID folderId, - @RequestPart("file") MultipartFile file); + @Operation(summary = "Upload a file to a specific folder", description = "This method uploads a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "201", description = "File uploaded successfully", content = @Content(schema = @Schema(implementation = CreateFileResponse.class))) + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "413", description = "File is too large", content = @Content(schema = @Schema(implementation = ApiError.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + ResponseEntity create( + @RequestParam("folderId") UUID folderId, + @RequestParam("file") MultipartFile file); - @GetMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) - @Operation(summary = "Retrive a file", description = "This method retrive a file", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "File retrived successfully", content = @Content(schema = @Schema(implementation = GetFileResponse.class))), - @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity getById(@PathVariable(required = true) UUID id); + @Operation(summary = "Delete a file", description = "This method deletes a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "204", description = "File deleted successfully") + @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @DeleteMapping("{id}") + ResponseEntity delete(@PathVariable UUID id); - @GetMapping(value = "{id}/download", produces = { MediaType.APPLICATION_OCTET_STREAM_VALUE }) - @Operation(summary = "Download a file", description = "This method downloads a file", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "File downloaded successfully", content = @Content(schema = @Schema(implementation = Resource.class))), - @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity download(@PathVariable(required = true) UUID id); + @Operation(summary = "Retrive a file", description = "This method retrive a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "200", description = "File retrieved successfully", content = @Content(schema = @Schema(implementation = GetFileResponse.class))) + @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("{id}") + ResponseEntity getById(@PathVariable UUID id); - @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE }) - @Operation(summary = "List files", description = "This method list files", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Files listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { - FileListResponse.class }))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity> list( - @RequestParam(name = "page", required = false, defaultValue = "0") final int page, - @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, - @RequestParam(name = "orderField", required = false, defaultValue = "createdAt") String orderField, - @RequestParam(name = "orderDirection", required = false, defaultValue = "DESC") Pagination.Order.Direction orderDirection, - @RequestParam(name = "filterOperator", required = false, defaultValue = "AND") Filter.Operator filterOperator, - @RequestParam(name = "filters", required = false) List filters); + @Operation(summary = "Download a file", description = "This method downloads a file", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "200", description = "File downloaded successfully", content = @Content(schema = @Schema(implementation = Resource.class))) + @ApiResponse(responseCode = "404", description = "File not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("{id}/content") + ResponseEntity download(@PathVariable UUID id); + + @Operation(summary = "List files", description = "This method list files", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "200", description = "Files listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { + FileListResponse.class }))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping + ResponseEntity> list( + @RequestParam(name = "page", required = false, defaultValue = "0") final int page, + @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, + @RequestParam(name = "orderField", required = false, defaultValue = "createdAt") String orderField, + @RequestParam(name = "orderDirection", required = false, defaultValue = "DESC") Pagination.Order.Direction orderDirection, + @RequestParam(name = "filterOperator", required = false, defaultValue = "AND") Filter.Operator filterOperator, + @RequestParam(name = "filters", required = false) List filters); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java index 62856f1b..0c5a9817 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/FolderAPI.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.UUID; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -21,14 +20,12 @@ import com.callv2.drive.infrastructure.folder.model.CreateFolderResponse; import com.callv2.drive.infrastructure.folder.model.FolderListResponse; import com.callv2.drive.infrastructure.folder.model.GetFolderResponse; -import com.callv2.drive.infrastructure.folder.model.GetRootFolderResponse; import com.callv2.drive.infrastructure.folder.model.MoveFolderRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -36,51 +33,39 @@ @RequestMapping("folders") public interface FolderAPI { - @GetMapping(value = "root", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a folder", description = "This method retrive a root folder", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Root folder retrived successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) - ResponseEntity getRoot(); + @ApiResponse(responseCode = "200", description = "Root folder retrieved successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("root") + ResponseEntity getRoot(); - @PostMapping(consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { - MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Create a folder", description = "This method creates a folder", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "Folder created successfully", content = @Content(schema = @Schema(implementation = CreateFolderResponse.class))), - @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "201", description = "Folder created successfully", content = @Content(schema = @Schema(implementation = CreateFolderResponse.class))) + @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @PostMapping ResponseEntity create(@RequestBody CreateFolderRequest request); - @GetMapping(value = "{id}", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Retrive a folder", description = "This method retrive a folder", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Folder retrived successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))), - @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "200", description = "Folder retrieved successfully", content = @Content(schema = @Schema(implementation = GetFolderResponse.class))) + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("{id}") ResponseEntity getById(@PathVariable(required = true) UUID id); - @PatchMapping(value = "{id}/move", consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { - MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "Move a folder", description = "This method moves a folder to a new location", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "Folder moved successfully", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))), - @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "204", description = "Folder moved successfully", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @PatchMapping("{id}/parent") ResponseEntity move(@PathVariable(required = true) UUID id, @RequestBody MoveFolderRequest request); - @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "List folders", description = "This method list folders", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Folders listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { - FolderListResponse.class }))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "200", description = "Folders listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { + FolderListResponse.class }))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping ResponseEntity> list( @RequestParam(name = "page", required = false, defaultValue = "0") final int page, @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, @@ -89,4 +74,12 @@ ResponseEntity> list( @RequestParam(name = "filterOperator", required = false, defaultValue = "AND") Filter.Operator filterOperator, @RequestParam(name = "filters", required = false) List filters); + @Operation(summary = "Change folder name", description = "This method changes the name of a folder", security = @SecurityRequirement(name = "bearerAuth")) + @ApiResponse(responseCode = "204", description = "Folder name changed successfully", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "404", description = "Folder not found", content = @Content(schema = @Schema(implementation = Void.class))) + @ApiResponse(responseCode = "422", description = "A validation error was thrown", content = @Content(schema = @Schema(implementation = ApiError.class))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @PatchMapping("{id}/name") + ResponseEntity changeName(@PathVariable(required = true) UUID id, @RequestBody String request); + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAPI.java index c7e48c99..da733ae1 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAPI.java @@ -14,7 +14,6 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -22,22 +21,18 @@ @RequestMapping("members") public interface MemberAPI { - @PostMapping("quotas/requests/{amount}") @Operation(summary = "Request drive quota", description = "This method request a drive ammount quota", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "Requested successfuly"), - @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - }) + @ApiResponse(responseCode = "204", description = "Requested successfuly") + @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) + @PostMapping("quotas/requests/{amount}") ResponseEntity requestQuota( @PathVariable(value = "amount", required = true) long amount, @RequestParam(value = "unit", defaultValue = "GIGABYTE") QuotaUnit unit); - @GetMapping("quotas") @Operation(summary = "Retrieve actual drive quota", description = "This method retrieve a drive ammount quota", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Retrieve successfuly"), - @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - }) + @ApiResponse(responseCode = "200", description = "Retrieve successfuly") + @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) + @GetMapping("quotas") ResponseEntity getQuota(); } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java index f24c99a3..f3ea018a 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/MemberAdminAPI.java @@ -1,10 +1,9 @@ package com.callv2.drive.infrastructure.api; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -18,7 +17,6 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,35 +24,29 @@ @RequestMapping("admin/members") public interface MemberAdminAPI { - @GetMapping("{id}/quotas") @Operation(summary = "Request drive quota", description = "This method request a drive ammount quota", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Retrieve successfuly"), - @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - }) + @ApiResponse(responseCode = "200", description = "Retrieve successfuly") + @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) + @GetMapping("{id}/quotas") ResponseEntity getQuota(@PathVariable(value = "id", required = true) String id); - @PostMapping("{id}/quotas/requests/approve") @Operation(summary = "Approve drive quota request", description = "This method approve a drive ammount quota request", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "Approved successfuly"), - @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) - }) + @ApiResponse(responseCode = "204", description = "Approved successfuly") + @ApiResponse(responseCode = "404", description = "Member not found", content = @Content(schema = @Schema(implementation = Void.class))) + @PatchMapping("{id}/quotas/requests") ResponseEntity approveQuotaRequest( @PathVariable(value = "id", required = true) String id, @RequestParam(value = "approved", defaultValue = "true") boolean approved); - @GetMapping(value = "quotas/requests", produces = { MediaType.APPLICATION_JSON_VALUE }) @Operation(summary = "List quotas requests", description = "This method list quotas requests", security = @SecurityRequirement(name = "bearerAuth")) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Quotas requests listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { - QuotaRequestListResponse.class }))), - @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) - }) + @ApiResponse(responseCode = "200", description = "Quotas requests listed successfully", content = @Content(schema = @Schema(implementation = Page.class, subTypes = { + QuotaRequestListResponse.class }))) + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = ApiError.class))) + @GetMapping("quotas/requests") ResponseEntity> listQuotaRequests( @RequestParam(name = "page", required = false, defaultValue = "0") final int page, @RequestParam(name = "perPage", required = false, defaultValue = "10") final int perPage, - @RequestParam(name = "orderField", required = false, defaultValue = "requestedAt") String orderField, + @RequestParam(name = "orderField", required = false, defaultValue = "quotaRequestedAt") String orderField, @RequestParam(name = "orderDirection", required = false, defaultValue = "DESC") Pagination.Order.Direction orderDirection); } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/ApiError.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/ApiError.java index 06e7ffdc..4daf26da 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/ApiError.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/ApiError.java @@ -3,15 +3,14 @@ import java.util.List; import com.callv2.drive.domain.exception.DomainException; -import com.callv2.drive.domain.validation.Error; -public record ApiError(String message, List errors) { +public record ApiError(String message, List errors) { static ApiError with(final String message) { return new ApiError(message, List.of()); } static ApiError from(final DomainException ex) { - return new ApiError(ex.getMessage(), ex.getErrors()); + return new ApiError(ex.getMessage(), ex.getErrors().stream().map(DomainException.Error::message).toList()); } } \ No newline at end of file diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java index 5fb9edb0..e7a6db47 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FileController.java @@ -17,6 +17,8 @@ import com.callv2.drive.application.file.content.get.GetFileContentOutput; import com.callv2.drive.application.file.content.get.GetFileContentUseCase; import com.callv2.drive.application.file.create.CreateFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileInput; import com.callv2.drive.application.file.retrieve.get.GetFileInput; import com.callv2.drive.application.file.retrieve.get.GetFileUseCase; import com.callv2.drive.application.file.retrieve.list.ListFilesUseCase; @@ -37,16 +39,19 @@ public class FileController implements FileAPI { private final CreateFileUseCase createFileUseCase; + private final DeleteFileUseCase deleteFileUseCase; private final GetFileUseCase getFileUseCase; private final GetFileContentUseCase getFileContentUseCase; private final ListFilesUseCase listFilesUseCase; public FileController( final CreateFileUseCase createFileUseCase, + final DeleteFileUseCase deleteFileUseCase, final GetFileUseCase getFileUseCase, final GetFileContentUseCase getFileContentUseCase, final ListFilesUseCase listFilesUseCase) { this.createFileUseCase = createFileUseCase; + this.deleteFileUseCase = deleteFileUseCase; this.getFileUseCase = getFileUseCase; this.getFileContentUseCase = getFileContentUseCase; this.listFilesUseCase = listFilesUseCase; @@ -65,6 +70,16 @@ public ResponseEntity create(UUID folderId, MultipartFile fi .body(response); } + @Override + public ResponseEntity delete(UUID id) { + final var deleterId = SecurityContext.getAuthenticatedUser(); + + DeleteFileInput deleteFileInput = DeleteFileInput.of(deleterId, id); + + deleteFileUseCase.execute(deleteFileInput); + return ResponseEntity.noContent().build(); + } + @Override public ResponseEntity getById(UUID id) { return ResponseEntity.ok(FilePresenter.present(getFileUseCase.execute(GetFileInput.from(id)))); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/FolderController.java index 1a13f32e..9854733f 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 @@ -14,6 +14,8 @@ 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.application.folder.update.name.UpdateFolderNameInput; +import com.callv2.drive.application.folder.update.name.UpdateFolderNameUseCase; import com.callv2.drive.domain.pagination.Filter; import com.callv2.drive.domain.pagination.Page; import com.callv2.drive.domain.pagination.Pagination; @@ -25,7 +27,6 @@ import com.callv2.drive.infrastructure.folder.model.CreateFolderResponse; import com.callv2.drive.infrastructure.folder.model.FolderListResponse; import com.callv2.drive.infrastructure.folder.model.GetFolderResponse; -import com.callv2.drive.infrastructure.folder.model.GetRootFolderResponse; import com.callv2.drive.infrastructure.folder.model.MoveFolderRequest; import com.callv2.drive.infrastructure.folder.presenter.FolderPresenter; import com.callv2.drive.infrastructure.security.SecurityContext; @@ -38,22 +39,25 @@ public class FolderController implements FolderAPI { private final GetFolderUseCase getFolderUseCase; private final MoveFolderUseCase moveFolderUseCase; private final ListFoldersUseCase listFoldersUseCase; + private final UpdateFolderNameUseCase updateFolderNameUseCase; public FolderController( final GetRootFolderUseCase getRootFolderUseCase, final CreateFolderUseCase createFolderUseCase, final GetFolderUseCase getFolderUseCase, final MoveFolderUseCase moveFolderUseCase, - final ListFoldersUseCase listFoldersUseCase) { + final ListFoldersUseCase listFoldersUseCase, + final UpdateFolderNameUseCase updateFolderNameUseCase) { this.getRootFolderUseCase = getRootFolderUseCase; this.createFolderUseCase = createFolderUseCase; this.getFolderUseCase = getFolderUseCase; this.moveFolderUseCase = moveFolderUseCase; this.listFoldersUseCase = listFoldersUseCase; + this.updateFolderNameUseCase = updateFolderNameUseCase; } @Override - public ResponseEntity getRoot() { + public ResponseEntity getRoot() { return ResponseEntity.ok(FolderPresenter.present( getRootFolderUseCase.execute(GetRootFolderInput.from(SecurityContext.getAuthenticatedUser())))); } @@ -104,4 +108,12 @@ public ResponseEntity> list( return ResponseEntity.ok(listFoldersUseCase.execute(query).map(FolderPresenter::present)); } + @Override + public ResponseEntity changeName(UUID id, String newName) { + + this.updateFolderNameUseCase.execute(new UpdateFolderNameInput(id, newName)); + + return ResponseEntity.noContent().build(); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/GlobalExceptionHandler.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/GlobalExceptionHandler.java index 240f60d9..a04c449a 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/GlobalExceptionHandler.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/api/controller/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import com.callv2.drive.domain.exception.DomainException; import com.callv2.drive.domain.exception.InternalErrorException; +import com.callv2.drive.domain.exception.NotAllowedException; import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.exception.QuotaExceededException; import com.callv2.drive.domain.exception.ValidationException; @@ -17,7 +18,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(value = Throwable.class) public ResponseEntity handle(final Throwable ex) { - return ResponseEntity.internalServerError().body(ApiError.with("Internal Server Error" + ex.getMessage())); + return ResponseEntity.internalServerError().body(ApiError.with("Internal Server Error")); } @ExceptionHandler(value = InternalErrorException.class) @@ -51,4 +52,9 @@ public ResponseEntity handle(final InvalidDataAccessApiUsageException .body(ApiError.with("Invalid Data Access Api Usage [%s]".formatted(ex.getMessage()))); } + @ExceptionHandler(value = NotAllowedException.class) + public ResponseEntity handle(final NotAllowedException ex) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.from(ex)); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/aspect/AspectConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/aspect/AspectConfig.java deleted file mode 100644 index e76fca6c..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/aop/aspect/AspectConfig.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.callv2.drive.infrastructure.configuration.aop.aspect; - -import org.aopalliance.intercept.MethodInterceptor; -import org.apache.logging.log4j.Level; -import org.springframework.aop.Advisor; -import org.springframework.aop.aspectj.AspectJExpressionPointcut; -import org.springframework.aop.support.DefaultPointcutAdvisor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.callv2.drive.infrastructure.aop.aspects.chain.AspectExecutorChain; -import com.callv2.drive.infrastructure.aop.aspects.chain.MethodInvocationAspectExecutorChain; -import com.callv2.drive.infrastructure.aop.aspects.chain.PostInvocationAspectExecutorChain; -import com.callv2.drive.infrastructure.aop.aspects.executor.ArgsLogExecutor; -import com.callv2.drive.infrastructure.aop.aspects.executor.MethodSignatureLogExecutor; -import com.callv2.drive.infrastructure.aop.aspects.executor.PostTelemetryLogExecutor; -import com.callv2.drive.infrastructure.aop.aspects.executor.ThrowableLogExecutor; -import com.callv2.drive.infrastructure.aop.aspects.handler.SimpleMethodInterceptorWithContextHandler; - -@Configuration -public class AspectConfig { - - @Bean - Advisor applicationAdvisor() { - - final var beforeChain = AspectExecutorChain.Builder - .create(MethodInvocationAspectExecutorChain.class) - .add(MethodInvocationAspectExecutorChain - .with(new MethodSignatureLogExecutor(Level.DEBUG, MethodSignatureLogExecutor.class))) - .add(MethodInvocationAspectExecutorChain - .with(new ArgsLogExecutor(Level.DEBUG, ArgsLogExecutor.class))) - .build(); - - final var afterChain = AspectExecutorChain.Builder - .create(PostInvocationAspectExecutorChain.class) - .add(PostInvocationAspectExecutorChain - .with(new PostTelemetryLogExecutor(Level.DEBUG, PostTelemetryLogExecutor.class))) - .build(); - - final var errorChain = AspectExecutorChain.Builder - .create(PostInvocationAspectExecutorChain.class) - .add(PostInvocationAspectExecutorChain - .with(new PostTelemetryLogExecutor(Level.ERROR, PostTelemetryLogExecutor.class))) - .add(PostInvocationAspectExecutorChain - .with(ThrowableLogExecutor.defaultCreate(ThrowableLogExecutor.class))) - .build(); - - return applicationAdvisor( - "execution(* com.callv2.drive.application..*.*(..))", - new SimpleMethodInterceptorWithContextHandler(beforeChain, afterChain, errorChain)); - } - - private static Advisor applicationAdvisor( - final String expression, - final MethodInterceptor interceptor) { - - final AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); - pointcut.setExpression(expression); - - return new DefaultPointcutAdvisor( - pointcut, - interceptor); - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java new file mode 100644 index 00000000..3825a6f2 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/messaging/RabbitMQConfig.java @@ -0,0 +1,74 @@ +package com.callv2.drive.infrastructure.configuration.messaging; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueBuilder; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + + @Bean + MessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } + + @Configuration + static class Admin { + + @Bean + TopicExchange memberExchange() { + return new TopicExchange("member.exchange"); + } + + @Bean + TopicExchange memberDlxExchange() { + return new TopicExchange("member.dlx.exchange"); + } + + @Bean + Queue memberSyncQueue() { + return QueueBuilder + .durable("member.sync.drive.queue") + .deadLetterExchange("member.dlx.exchange") + .deadLetterRoutingKey("member.sync.deadletter") + .build(); + } + + @Bean + Queue memberSyncDeadLetterQueue() { + return QueueBuilder + .durable("member.sync.drive.queue.deadletter") + .build(); + } + + @Bean + Binding memberCreatedSyncBindings( + @Qualifier("memberExchange") final TopicExchange exchange, + @Qualifier("memberSyncQueue") final Queue queue) { + return BindingBuilder.bind(queue).to(exchange).with("member.created"); + } + + @Bean + Binding memberUpdatedSyncBindings( + @Qualifier("memberExchange") final TopicExchange exchange, + @Qualifier("memberSyncQueue") final Queue queue) { + return BindingBuilder.bind(queue).to(exchange).with("member.updated"); + } + + @Bean + Binding memberSyncDeadLetterBinding( + @Qualifier("memberDlxExchange") final TopicExchange exchange, + @Qualifier("memberSyncDeadLetterQueue") final Queue queue) { + return BindingBuilder.bind(queue).to(exchange).with("member.sync.deadletter"); + } + + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/aspect/PointcutAdvisorProperties.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/aspect/PointcutAdvisorProperties.java deleted file mode 100644 index c930113e..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/properties/aspect/PointcutAdvisorProperties.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.callv2.drive.infrastructure.configuration.properties.aspect; - -import java.util.List; - -public class PointcutAdvisorProperties { - - private String expression; - - private List before; - private List after; - private List error; - - public PointcutAdvisorProperties() { - } - - public String getExpression() { - return expression; - } - - public void setExpression(String expression) { - this.expression = expression; - } - - public List getBefore() { - return before; - } - - public void setBefore(List before) { - this.before = before; - } - - public List getAfter() { - return after; - } - - public void setAfter(List after) { - this.after = after; - } - - public List getError() { - return error; - } - - public void setError(List error) { - this.error = error; - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java index c77d950c..37d798c5 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakAuthoritiesConverter.java @@ -10,6 +10,7 @@ import java.util.stream.Stream; import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -25,7 +26,7 @@ public class KeycloakAuthoritiesConverter implements Converter convert(final Jwt jwt) { + public Collection convert(@NonNull final Jwt jwt) { final var realmRoles = extractRealmRoles(jwt); final var resourceRoles = extractResourceRoles(jwt); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java index 2bf950b1..b8c89789 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/KeycloakJwtConverter.java @@ -3,6 +3,7 @@ import java.util.Collection; import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -18,7 +19,7 @@ public KeycloakJwtConverter() { } @Override - public AbstractAuthenticationToken convert(final Jwt jwt) { + public AbstractAuthenticationToken convert(@NonNull final Jwt jwt) { return new JwtAuthenticationToken(jwt, extractAuthorities(jwt), extractPrincipal(jwt)); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/SecurityConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/SecurityConfig.java index 6995bff3..77ffa143 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/SecurityConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/security/SecurityConfig.java @@ -22,7 +22,8 @@ @EnableMethodSecurity public class SecurityConfig { - private static final String ROLE_ADMIN = "ADMINISTRADOR"; + private static final String ROLE_ADMIN = "CALLV2_ADMIN"; + private static final String ROLE_MEMBER = "CALLV2_DRIVE_MEMBER"; @Bean SecurityFilterChain securityFilterChain( @@ -48,7 +49,7 @@ SecurityFilterChain securityFilterChain( .permitAll() .anyRequest() - .authenticated(); + .hasAnyRole(ROLE_MEMBER); }) .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer .jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtConverter()))) diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java index 1906d318..030eb267 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FileUseCaseConfig.java @@ -7,6 +7,8 @@ import com.callv2.drive.application.file.content.get.GetFileContentUseCase; import com.callv2.drive.application.file.create.CreateFileUseCase; import com.callv2.drive.application.file.create.DefaultCreateFileUseCase; +import com.callv2.drive.application.file.delete.DefaultDeleteFileUseCase; +import com.callv2.drive.application.file.delete.DeleteFileUseCase; import com.callv2.drive.application.file.retrieve.get.DefaultGetFileUseCase; import com.callv2.drive.application.file.retrieve.get.GetFileUseCase; import com.callv2.drive.application.file.retrieve.list.DefaultListFilesUseCase; @@ -40,6 +42,11 @@ CreateFileUseCase createFileUseCase() { return new DefaultCreateFileUseCase(memberGateway, folderGateway, fileGateway, storageService); } + @Bean + DeleteFileUseCase deleteFileUseCase() { + return new DefaultDeleteFileUseCase(memberGateway, fileGateway, storageService); + } + @Bean GetFileUseCase getFileUseCase() { return new DefaultGetFileUseCase(fileGateway); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FolderUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FolderUseCaseConfig.java index 960229c1..7312bf5d 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FolderUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/FolderUseCaseConfig.java @@ -13,6 +13,8 @@ import com.callv2.drive.application.folder.retrieve.get.root.GetRootFolderUseCase; import com.callv2.drive.application.folder.retrieve.list.DefaultListFoldersUseCase; import com.callv2.drive.application.folder.retrieve.list.ListFoldersUseCase; +import com.callv2.drive.application.folder.update.name.DefaultUpdateFolderNameUseCase; +import com.callv2.drive.application.folder.update.name.UpdateFolderNameUseCase; import com.callv2.drive.domain.file.FileGateway; import com.callv2.drive.domain.folder.FolderGateway; import com.callv2.drive.domain.member.MemberGateway; @@ -58,4 +60,9 @@ ListFoldersUseCase listFoldersUseCase() { return new DefaultListFoldersUseCase(folderGateway); } + @Bean + UpdateFolderNameUseCase updateFolderNameUseCase() { + return new DefaultUpdateFolderNameUseCase(folderGateway); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java index 4aa4b4aa..14d4ac06 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/configuration/usecase/MemberUseCaseConfig.java @@ -11,6 +11,8 @@ import com.callv2.drive.application.member.quota.request.list.ListRequestQuotaUseCase; import com.callv2.drive.application.member.quota.retrieve.get.DefaultGetQuotaUseCase; import com.callv2.drive.application.member.quota.retrieve.get.GetQuotaUseCase; +import com.callv2.drive.application.member.synchronize.DefaultSynchronizeMemberUseCase; +import com.callv2.drive.application.member.synchronize.SynchronizeMemberUseCase; import com.callv2.drive.domain.file.FileGateway; import com.callv2.drive.domain.member.MemberGateway; @@ -45,4 +47,9 @@ GetQuotaUseCase getQuotaUseCase() { return new DefaultGetQuotaUseCase(memberGateway, fileGateway); } + @Bean + SynchronizeMemberUseCase synchronizeMemberUseCase() { + return new DefaultSynchronizeMemberUseCase(memberGateway); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/converter/JacksonCaster.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/converter/JacksonCaster.java index d420a4f5..f7d0b9f7 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/converter/JacksonCaster.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/converter/JacksonCaster.java @@ -6,13 +6,10 @@ import com.callv2.drive.infrastructure.configuration.mapper.Mapper; @Component -public final class JacksonCaster implements Caster { +public class JacksonCaster implements Caster { private static final ObjectMapper mapper = Mapper.mapper(); - private JacksonCaster() { - } - @Override public T cast(Object value, Class targetType) { return mapper.convertValue(value, targetType); diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java index 35cb8815..825302a8 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/FileJPAGateway.java @@ -61,7 +61,7 @@ public Page findAll(final SearchQuery searchQuery) { searchQuery.filters()); final org.springframework.data.domain.Page pageResult = this.fileRepository - .findAll(Specification.where(specification), page); + .findAll(specification, page); return new Page<>( pageResult.getNumber(), @@ -81,4 +81,9 @@ public List findByOwner(MemberID ownerId) { .toList(); } + @Override + public void deleteById(final FileID id) { + this.fileRepository.deleteById(id.getValue()); + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/persistence/FileJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/persistence/FileJpaEntity.java index 2785ce24..bdb06259 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/persistence/FileJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/file/persistence/FileJpaEntity.java @@ -166,4 +166,11 @@ public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + @Override + public String toString() { + return "FileJpaEntity [id=" + id + ", ownerId=" + ownerId + ", folderId=" + folderId + ", name=" + name + + ", contentType=" + contentType + ", contentLocation=" + contentLocation + ", contentSize=" + + contentSize + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + "]"; + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/filter/FilterService.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/filter/FilterService.java index c95c8fd7..5e58773f 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/filter/FilterService.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/filter/FilterService.java @@ -23,12 +23,12 @@ public Specification buildSpecification( final List filters) { if (filterMethod.equals(Filter.Operator.AND)) - return Specification.where(andSpecifications(buildSpecifications(entityClass, filters))); + return andSpecifications(buildSpecifications(entityClass, filters)); if (filterMethod.equals(Filter.Operator.OR)) - return Specification.where(orSpecifications(buildSpecifications(entityClass, filters))); + return orSpecifications(buildSpecifications(entityClass, filters)); - return Specification.where(andSpecifications(buildSpecifications(entityClass, filters))); + return andSpecifications(buildSpecifications(entityClass, filters)); } private List> buildSpecifications(Class entityClass, diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java index 281a9ce0..bd2f6843 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/FolderJpaGateway.java @@ -2,6 +2,8 @@ import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; @@ -35,6 +37,15 @@ public Optional findRoot() { return this.folderRepository.findByRootFolderTrue().map(FolderJpaEntity::toDomain); } + @Override + public Set findByParentFolderId(FolderID parentFolderId) { + return this.folderRepository + .findAllByParentFolderId(parentFolderId.getValue()) + .stream() + .map(FolderJpaEntity::toDomain) + .collect(Collectors.toSet()); + } + @Override public Folder create(Folder folder) { return save(folder); @@ -70,7 +81,7 @@ public Page findAll(SearchQuery searchQuery) { searchQuery.filters()); final org.springframework.data.domain.Page pageResult = this.folderRepository.findAll( - Specification.where(specification), + specification, page); return new Page<>( diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/FolderListResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/FolderListResponse.java index 106da8be..a1dbe714 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/FolderListResponse.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/FolderListResponse.java @@ -1,16 +1,10 @@ package com.callv2.drive.infrastructure.folder.model; -import java.util.List; import java.util.UUID; public record FolderListResponse( UUID id, String name, - UUID parentFolder, - List subFolders) { - - public static record SubFolder(UUID id, String name) { - - } + UUID parentFolder) { } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetFolderResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetFolderResponse.java index 3b5c9274..9f598e37 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetFolderResponse.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetFolderResponse.java @@ -1,15 +1,17 @@ package com.callv2.drive.infrastructure.folder.model; import java.time.Instant; -import java.util.List; +import java.util.Set; import java.util.UUID; public record GetFolderResponse( UUID id, String name, + Boolean rootFolder, UUID parentFolder, - List subFolders, - List files, + Set subFolders, + Set files, + String ownerId, Instant createdAt, Instant updatedAt) { diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetRootFolderResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetRootFolderResponse.java deleted file mode 100644 index 017ab681..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/model/GetRootFolderResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.callv2.drive.infrastructure.folder.model; - -import java.time.Instant; -import java.util.List; -import java.util.UUID; - -public record GetRootFolderResponse( - UUID id, - List subFolders, - List files, - Instant createdAt) { - - public static record SubFolder(UUID id, String name) { - } - - public static record File( - UUID id, - String name, - Long size, - Instant createdAt, - Instant updatedAt) { - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaEntity.java index 53eb4d6d..87117222 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaEntity.java @@ -1,22 +1,16 @@ package com.callv2.drive.infrastructure.folder.persistence; import java.time.Instant; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; import com.callv2.drive.domain.folder.Folder; import com.callv2.drive.domain.folder.FolderID; import com.callv2.drive.domain.folder.FolderName; -import com.callv2.drive.domain.folder.SubFolder; import com.callv2.drive.domain.member.MemberID; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity(name = "Folder") @@ -47,9 +41,6 @@ public class FolderJpaEntity { @Column(name = "deleted_at") private Instant deletedAt; - @OneToMany(mappedBy = "parentFolder", cascade = CascadeType.ALL, orphanRemoval = true) - private Set subFolders; - private FolderJpaEntity( final UUID id, final Boolean rootFolder, @@ -68,7 +59,6 @@ private FolderJpaEntity( this.updatedAt = updatedAt; this.deletedAt = deletedAt; - this.subFolders = new HashSet<>(); } public FolderJpaEntity() { @@ -87,9 +77,6 @@ public static FolderJpaEntity fromDomain(final Folder folder) { folder.getUpdatedAt(), folder.getDeletedAt()); - folder.getSubFolders() - .forEach(entity::addSubFolder); - return entity; } @@ -99,19 +86,12 @@ public Folder toDomain() { MemberID.of(ownerId), FolderName.of(name), FolderID.of(parentFolderId), - subFolders.stream() - .map(SubFolderJpaEntity::toDomain) - .collect(Collectors.toSet()), createdAt, updatedAt, deletedAt, rootFolder); } - public void addSubFolder(final SubFolder anId) { - this.subFolders.add(SubFolderJpaEntity.with(this, anId)); - } - public UUID getId() { return id; } @@ -152,14 +132,6 @@ public void setParentFolderId(UUID parentFolderId) { this.parentFolderId = parentFolderId; } - public Set getSubFolders() { - return subFolders; - } - - public void setSubFolders(Set subFolders) { - this.subFolders = subFolders; - } - public Instant getCreatedAt() { return createdAt; } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaRepository.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaRepository.java index 75de0db4..a3594240 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaRepository.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/FolderJpaRepository.java @@ -1,5 +1,6 @@ package com.callv2.drive.infrastructure.folder.persistence; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -12,6 +13,8 @@ public interface FolderJpaRepository extends JpaRepository findByRootFolderTrue(); + List findAllByParentFolderId(UUID parentFolderId); + Page findAll(Specification whereClause, Pageable page); } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderID.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderID.java deleted file mode 100644 index 44567fc7..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderID.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.callv2.drive.infrastructure.folder.persistence; - -import java.io.Serializable; -import java.util.UUID; - -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; - -@Embeddable -public class SubFolderID implements Serializable { - - @Column(name = "parent_folder_id", nullable = false) - private UUID parentFolderId; - - @Column(name = "sub_folder_id", nullable = false) - private UUID subFolderId; - - public SubFolderID() { - } - - private SubFolderID(UUID parentFolderId, UUID subFolderId) { - this.parentFolderId = parentFolderId; - this.subFolderId = subFolderId; - } - - public static SubFolderID with(UUID parentFolderId, UUID subFolderId) { - return new SubFolderID(parentFolderId, subFolderId); - } - - public UUID getParentFolderId() { - return parentFolderId; - } - - public void setParentFolderId(UUID parentFolderId) { - this.parentFolderId = parentFolderId; - } - - public UUID getSubFolderId() { - return subFolderId; - } - - public void setSubFolderId(UUID subFolderId) { - this.subFolderId = subFolderId; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((parentFolderId == null) ? 0 : parentFolderId.hashCode()); - result = prime * result + ((subFolderId == null) ? 0 : subFolderId.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SubFolderID other = (SubFolderID) obj; - if (parentFolderId == null) { - if (other.parentFolderId != null) - return false; - } else if (!parentFolderId.equals(other.parentFolderId)) - return false; - if (subFolderId == null) { - if (other.subFolderId != null) - return false; - } else if (!subFolderId.equals(other.subFolderId)) - return false; - return true; - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderJpaEntity.java deleted file mode 100644 index 1887135a..00000000 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/persistence/SubFolderJpaEntity.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.callv2.drive.infrastructure.folder.persistence; - -import com.callv2.drive.domain.folder.FolderID; -import com.callv2.drive.domain.folder.FolderName; -import com.callv2.drive.domain.folder.SubFolder; - -import jakarta.persistence.Column; -import jakarta.persistence.EmbeddedId; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.MapsId; -import jakarta.persistence.Table; - -@Entity(name = "SubFolder") -@Table(name = "sub_folders") -public class SubFolderJpaEntity { - - @EmbeddedId - private SubFolderID id; - - @Column(name = "name", nullable = false) - private String name; - - @ManyToOne(fetch = FetchType.LAZY) - @MapsId("parentFolderId") - private FolderJpaEntity parentFolder; - - public SubFolderJpaEntity() { - } - - private SubFolderJpaEntity( - final SubFolderID id, - final String name, - final FolderJpaEntity parentFolder) { - this.id = id; - this.name = name; - this.parentFolder = parentFolder; - } - - public static SubFolderJpaEntity with(final FolderJpaEntity folder, final SubFolder subFolder) { - return new SubFolderJpaEntity( - SubFolderID.with(folder.getId(), subFolder.id().getValue()), - subFolder.name().value(), - folder); - } - - public SubFolder toDomain() { - return SubFolder.with(FolderID.of(getId().getSubFolderId()), FolderName.of(getName())); - } - - public SubFolderID getId() { - return id; - } - - public void setId(SubFolderID id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public FolderJpaEntity getParentFolder() { - return parentFolder; - } - - public void setParentFolder(FolderJpaEntity parentFolder) { - this.parentFolder = parentFolder; - } - -} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java index 479251f0..257ae6f3 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/folder/presenter/FolderPresenter.java @@ -1,5 +1,7 @@ package com.callv2.drive.infrastructure.folder.presenter; +import java.util.stream.Collectors; + import com.callv2.drive.application.folder.create.CreateFolderOutput; import com.callv2.drive.application.folder.retrieve.get.GetFolderOutput; import com.callv2.drive.application.folder.retrieve.get.root.GetRootFolderOutput; @@ -7,60 +9,70 @@ import com.callv2.drive.infrastructure.folder.model.CreateFolderResponse; import com.callv2.drive.infrastructure.folder.model.FolderListResponse; import com.callv2.drive.infrastructure.folder.model.GetFolderResponse; -import com.callv2.drive.infrastructure.folder.model.GetRootFolderResponse; public interface FolderPresenter { - static CreateFolderResponse present(CreateFolderOutput output) { + static CreateFolderResponse present(final CreateFolderOutput output) { return new CreateFolderResponse(output.id()); } - static GetFolderResponse present(GetFolderOutput output) { + static GetFolderResponse present(final GetFolderOutput output) { return new GetFolderResponse( output.id(), output.name(), + output.isRootFolder(), output.parentFolder(), output.subFolders().stream() .map(FolderPresenter::present) - .toList(), + .collect(Collectors.toSet()), output.files() .stream() .map(FolderPresenter::present) - .toList(), + .collect(Collectors.toSet()), + output.ownerId(), output.createdAt(), output.updatedAt()); } - static GetFolderResponse.SubFolder present(GetFolderOutput.SubFolder subFolder) { + static GetFolderResponse.SubFolder present(final GetFolderOutput.SubFolder subFolder) { return new GetFolderResponse.SubFolder(subFolder.id(), subFolder.name()); } - static GetFolderResponse.File present(GetFolderOutput.File subFolder) { + static GetFolderResponse.File present(final GetFolderOutput.File file) { return new GetFolderResponse.File( - subFolder.id(), - subFolder.name(), - subFolder.size(), - subFolder.createdAt(), - subFolder.updatedAt()); + file.id(), + file.name(), + file.size(), + file.createdAt(), + file.updatedAt()); } - static GetRootFolderResponse present(GetRootFolderOutput output) { - return new GetRootFolderResponse( + static GetFolderResponse present(final GetRootFolderOutput output) { + return new GetFolderResponse( output.id(), - output.subFolders().stream().map(FolderPresenter::present).toList(), + output.name(), + true, + null, + output + .subFolders() + .stream() + .map(FolderPresenter::present) + .collect(Collectors.toSet()), output.files() .stream() .map(FolderPresenter::present) - .toList(), - output.createdAt()); + .collect(Collectors.toSet()), + output.ownerId(), + output.createdAt(), + output.updatedAt()); } - static GetRootFolderResponse.SubFolder present(GetRootFolderOutput.SubFolder subFolder) { - return new GetRootFolderResponse.SubFolder(subFolder.id(), subFolder.name()); + static GetFolderResponse.SubFolder present(final GetRootFolderOutput.SubFolder subFolder) { + return new GetFolderResponse.SubFolder(subFolder.id(), subFolder.name()); } - static GetRootFolderResponse.File present(GetRootFolderOutput.File subFolder) { - return new GetRootFolderResponse.File( + static GetFolderResponse.File present(final GetRootFolderOutput.File subFolder) { + return new GetFolderResponse.File( subFolder.id(), subFolder.name(), subFolder.size(), @@ -68,16 +80,20 @@ static GetRootFolderResponse.File present(GetRootFolderOutput.File subFolder) { subFolder.updatedAt()); } - static FolderListResponse present(FolderListOutput output) { + static GetFolderResponse.File present(final GetFolderResponse.File file) { + return new GetFolderResponse.File( + file.id(), + file.name(), + file.size(), + file.createdAt(), + file.updatedAt()); + } + + static FolderListResponse present(final FolderListOutput output) { return new FolderListResponse( output.id(), output.name(), - output.parentFolder(), - output.subFolders().stream().map(FolderPresenter::present).toList()); - } - - static FolderListResponse.SubFolder present(FolderListOutput.SubFolder subFolder) { - return new FolderListResponse.SubFolder(subFolder.id(), subFolder.name()); + output.parentFolder()); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java index 12a0000f..ff1a698c 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/DefaultMemberGateway.java @@ -2,8 +2,12 @@ import java.util.Optional; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import com.callv2.drive.domain.exception.AlreadyExistsException; +import com.callv2.drive.domain.exception.NotFoundException; import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberGateway; import com.callv2.drive.domain.member.MemberID; @@ -25,7 +29,10 @@ public DefaultMemberGateway(final MemberJpaRepository memberJpaRepository) { @Override public Member create(final Member member) { - return save(member); + if (this.memberJpaRepository.existsById(member.getId().getValue())) + throw AlreadyExistsException.with(Member.class, member.getId().getValue()); + + return this.memberJpaRepository.save(MemberJpaEntity.fromDomain(member)).toDomain(); } @Override @@ -35,15 +42,32 @@ public Optional findById(final MemberID id) { .map(MemberJpaEntity::toDomain); } + @Transactional @Override public Member update(final Member member) { - return save(member); - } - private Member save(final Member member) { - return memberJpaRepository - .save(MemberJpaEntity.fromDomain(member)) - .toDomain(); + if (!this.memberJpaRepository.existsById(member.getId().getValue())) + throw NotFoundException.with(Member.class, member.getId().getValue()); + + final MemberJpaEntity memberJpa = MemberJpaEntity.fromDomain(member); + final Integer rowsUpdated = memberJpaRepository.update( + memberJpa.getId(), + memberJpa.getUsername(), + memberJpa.getNickname(), + memberJpa.getQuotaAmmount(), + memberJpa.getQuotaUnit(), + memberJpa.getQuotaRequestAmmount(), + memberJpa.getQuotaRequestUnit(), + memberJpa.getQuotaRequestedAt(), + memberJpa.getCreatedAt(), + memberJpa.getUpdatedAt(), + memberJpa.getSynchronizedVersion()); + + if (rowsUpdated != 1) + throw new OptimisticLockingFailureException( + "Member update failed due to version conflict for id: " + member.getId().getValue()); + + return member; } @Override diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/MemberQuotaResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/MemberQuotaResponse.java index 58db7ce3..c496b451 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/MemberQuotaResponse.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/MemberQuotaResponse.java @@ -1,5 +1,5 @@ package com.callv2.drive.infrastructure.member.model; -public record MemberQuotaResponse(Long used, Long total, Long available) { +public record MemberQuotaResponse(String memberId, Long used, Long total, Long available) { } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/QuotaRequestListResponse.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/QuotaRequestListResponse.java index 815dc821..4ffbf416 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/QuotaRequestListResponse.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/model/QuotaRequestListResponse.java @@ -5,9 +5,12 @@ import com.callv2.drive.domain.member.QuotaUnit; public record QuotaRequestListResponse( - String memberId, + Member member, long amount, QuotaUnit unit, Instant requestedAt) { + public record Member(String id, String username) { + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java index 15b72e4d..ddb197c9 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaEntity.java @@ -4,9 +4,11 @@ import com.callv2.drive.domain.member.Member; import com.callv2.drive.domain.member.MemberID; +import com.callv2.drive.domain.member.Nickname; import com.callv2.drive.domain.member.Quota; import com.callv2.drive.domain.member.QuotaRequest; import com.callv2.drive.domain.member.QuotaUnit; +import com.callv2.drive.domain.member.Username; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -22,6 +24,10 @@ public class MemberJpaEntity { @Id private String id; + private String username; + + private String nickname; + @Column(nullable = false) private Long quotaAmmount; @@ -36,27 +42,40 @@ public class MemberJpaEntity { private Instant quotaRequestedAt; + @Column(nullable = false) + private Boolean hasSystemAccess; + private Instant createdAt; private Instant updatedAt; + private Long synchronizedVersion; + public MemberJpaEntity( final String id, + final String username, + final String nickname, final Long quotaAmmount, final QuotaUnit quotaUnit, final Long quotaRequestAmmount, final QuotaUnit quotaRequestUnit, final Instant quotaRequestedAt, + final Boolean hasSystemAccess, final Instant createdAt, - final Instant updatedAt) { + final Instant updatedAt, + final Long synchronizedVersion) { this.id = id; + this.username = username; + this.nickname = nickname; this.quotaAmmount = quotaAmmount; this.quotaUnit = quotaUnit; this.quotaRequestAmmount = quotaRequestAmmount; this.quotaRequestUnit = quotaRequestUnit; this.quotaRequestedAt = quotaRequestedAt; + this.hasSystemAccess = hasSystemAccess == null ? false : hasSystemAccess; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.synchronizedVersion = synchronizedVersion; } public MemberJpaEntity() { @@ -72,22 +91,30 @@ public Member toDomain() { return Member.with( MemberID.of(getId()), + Username.of(getUsername()), + Nickname.of(getNickname()), Quota.of(getQuotaAmmount(), getQuotaUnit()), quotaRequest, + getHasSystemAccess(), getCreatedAt(), - getUpdatedAt()); + getUpdatedAt(), + getSynchronizedVersion()); } public static MemberJpaEntity fromDomain(final Member member) { return new MemberJpaEntity( member.getId().getValue(), + member.getUsername().value(), + member.getNickname().value(), member.getQuota().amount(), member.getQuota().unit(), member.getQuotaRequest().map(QuotaRequest::quota).map(Quota::amount).orElse(null), member.getQuotaRequest().map(QuotaRequest::quota).map(Quota::unit).orElse(null), member.getQuotaRequest().map(QuotaRequest::requesteddAt).orElse(null), + member.hasSystemAccess(), member.getCreatedAt(), - member.getUpdatedAt()); + member.getUpdatedAt(), + member.getSynchronizedVersion()); } public String getId() { @@ -98,6 +125,22 @@ public void setId(String id) { this.id = id; } + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + public Long getQuotaAmmount() { return quotaAmmount; } @@ -138,6 +181,14 @@ public void setQuotaRequestedAt(Instant quotaRequestedAt) { this.quotaRequestedAt = quotaRequestedAt; } + public Boolean getHasSystemAccess() { + return hasSystemAccess; + } + + public void setHasSystemAccess(Boolean hasSystemAccess) { + this.hasSystemAccess = hasSystemAccess; + } + public Instant getCreatedAt() { return createdAt; } @@ -154,4 +205,12 @@ public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + public Long getSynchronizedVersion() { + return synchronizedVersion; + } + + public void setSynchronizedVersion(Long synchronizedVersion) { + this.synchronizedVersion = synchronizedVersion; + } + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java index cd56fd07..efe13292 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/persistence/MemberJpaRepository.java @@ -1,20 +1,27 @@ package com.callv2.drive.infrastructure.member.persistence; +import java.time.Instant; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.callv2.drive.domain.member.QuotaRequestPreview; +import com.callv2.drive.domain.member.QuotaUnit; public interface MemberJpaRepository extends JpaRepository { @Query(""" select distinct new com.callv2.drive.domain.member.QuotaRequestPreview( m.id as memberId, - m.quotaRequestAmmount as amount, - m.quotaRequestUnit as unit, - m.quotaRequestedAt as requestedAt + m.username as memberUsername, + m.nickname as memberNickname, + m.quotaRequestAmmount as quotaAmount, + m.quotaRequestUnit as quotaUnit, + m.quotaRequestedAt as quotaRequestedAt ) from Member m where @@ -22,4 +29,34 @@ public interface MemberJpaRepository extends JpaRepository findAllQuotaRequests(Pageable page); + @Modifying + @Query(""" + update Member m + set + m.username = :username, + m.nickname = :nickname, + m.quotaAmmount = :quotaAmmount, + m.quotaUnit = :quotaUnit, + m.quotaRequestAmmount = :quotaRequestAmmount, + m.quotaRequestUnit = :quotaRequestUnit, + m.quotaRequestedAt = :quotaRequestedAt, + m.createdAt = :createdAt, + m.updatedAt = :updatedAt, + m.synchronizedVersion = :synchronizedVersion + where m.id = :id + and (m.synchronizedVersion is null or :synchronizedVersion >= m.synchronizedVersion) + """) + Integer update( + @Param("id") String id, + @Param("username") String username, + @Param("nickname") String nickname, + @Param("quotaAmmount") Long quotaAmmount, + @Param("quotaUnit") QuotaUnit quotaUnit, + @Param("quotaRequestAmmount") Long quotaRequestAmmount, + @Param("quotaRequestUnit") QuotaUnit quotaRequestUnit, + @Param("quotaRequestedAt") Instant quotaRequestedAt, + @Param("createdAt") Instant createdAt, + @Param("updatedAt") Instant updatedAt, + @Param("synchronizedVersion") Long synchronizedVersion); + } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/presenter/MemberPresenter.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/presenter/MemberPresenter.java index a57a7ce2..a60736c5 100644 --- a/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/presenter/MemberPresenter.java +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/member/presenter/MemberPresenter.java @@ -1,22 +1,24 @@ package com.callv2.drive.infrastructure.member.presenter; -import com.callv2.drive.application.member.quota.request.list.RequestQuotaListOutput; +import com.callv2.drive.application.member.quota.request.list.ListRequestQuotaOutput; import com.callv2.drive.application.member.quota.retrieve.get.GetQuotaOutput; import com.callv2.drive.infrastructure.member.model.MemberQuotaResponse; import com.callv2.drive.infrastructure.member.model.QuotaRequestListResponse; public interface MemberPresenter { - static QuotaRequestListResponse present(RequestQuotaListOutput output) { + static QuotaRequestListResponse present(final ListRequestQuotaOutput output) { return new QuotaRequestListResponse( - output.memberId(), - output.amount(), - output.unit(), - output.requestedAt()); + new QuotaRequestListResponse.Member( + output.memberId(), + output.memberUsername()), + output.quotaAmount(), + output.quotaUnit(), + output.quotaRequestedAt()); } - static MemberQuotaResponse present(GetQuotaOutput output) { - return new MemberQuotaResponse(output.used(), output.total(), output.available()); + static MemberQuotaResponse present(final GetQuotaOutput output) { + return new MemberQuotaResponse(output.memberId(), output.used(), output.total(), output.available()); } } diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/Listener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/Listener.java new file mode 100644 index 00000000..a94e6de6 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/Listener.java @@ -0,0 +1,8 @@ +package com.callv2.drive.infrastructure.messaging.listener; + +@FunctionalInterface +public interface Listener { + + void handle(T data); + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java new file mode 100644 index 00000000..fd4f296e --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncEvent.java @@ -0,0 +1,26 @@ +package com.callv2.drive.infrastructure.messaging.listener.member; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Set; + +public record MemberSyncEvent( + String id, + String source, + MemberSyncEvent.Data data, + Instant occurredAt) implements Serializable { + + public record Data( + String id, + String username, + String email, + String nickname, + boolean isActive, + Set systems, + Instant createdAt, + Instant updatedAt, + Long synchronizedVersion) implements Serializable { + + } + +} diff --git a/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java new file mode 100644 index 00000000..12ba39a1 --- /dev/null +++ b/infrastructure/src/main/java/com/callv2/drive/infrastructure/messaging/listener/member/MemberSyncListener.java @@ -0,0 +1,57 @@ +package com.callv2.drive.infrastructure.messaging.listener.member; + +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.stereotype.Component; + +import com.callv2.drive.application.member.synchronize.SynchronizeMemberInput; +import com.callv2.drive.application.member.synchronize.SynchronizeMemberUseCase; +import com.callv2.drive.domain.exception.AlreadyExistsException; +import com.callv2.drive.domain.exception.IdMismatchException; +import com.callv2.drive.domain.exception.SynchronizedVersionOutdatedException; +import com.callv2.drive.infrastructure.messaging.listener.Listener; + +@Component +public class MemberSyncListener implements Listener { + + private static final Logger log = LogManager.getLogger(MemberSyncListener.class); + + private final SynchronizeMemberUseCase synchronizeMemberUseCase; + + public MemberSyncListener(final SynchronizeMemberUseCase synchronizeMemberUseCase) { + this.synchronizeMemberUseCase = Objects.requireNonNull(synchronizeMemberUseCase); + } + + @Override + @RabbitListener(queues = "member.sync.drive.queue") + public void handle(final MemberSyncEvent data) { + + final MemberSyncEvent.Data eventData = data.data(); + + final Boolean hasSystemAccess = eventData.systems() != null && eventData.systems().contains("DRIVE"); + + final SynchronizeMemberInput createMemberInput = SynchronizeMemberInput.from( + eventData.id(), + eventData.username(), + eventData.nickname(), + hasSystemAccess, + eventData.createdAt(), + eventData.updatedAt(), + eventData.synchronizedVersion()); + + try { + synchronizeMemberUseCase.execute(createMemberInput); + } catch (OptimisticLockingFailureException + | SynchronizedVersionOutdatedException + | IdMismatchException + | AlreadyExistsException e) { + log.warn("Error synchronizing member: {}", e.getMessage(), e); + } + + } + +} diff --git a/infrastructure/src/main/resources/application-env.yml b/infrastructure/src/main/resources/application-env.yml index 8d6937b4..372edb20 100644 --- a/infrastructure/src/main/resources/application-env.yml +++ b/infrastructure/src/main/resources/application-env.yml @@ -26,6 +26,14 @@ db: request-timeout: ${REQUEST_TIMEOUT} +rabbitmq: + virtual-host: ${RABBITMQ_VIRTUAL_HOST:/} + auto-create: ${RABBITMQ_AUTO_CREATE} + host: ${RABBITMQ_HOST} + port: ${RABBITMQ_PORT} + username: ${RABBITMQ_USERNAME} + password: ${RABBITMQ_PASSWORD} + storage: max-file-size: ${MAX_FILE_SIZE} max-request-size: ${MAX_REQUEST_SIZE} diff --git a/infrastructure/src/main/resources/application.yml b/infrastructure/src/main/resources/application.yml index 2d954a2b..7bc79f30 100644 --- a/infrastructure/src/main/resources/application.yml +++ b/infrastructure/src/main/resources/application.yml @@ -32,5 +32,22 @@ spring: url: jdbc:postgresql://${postgres.host}:${postgres.port}/${postgres.database} jpa: hibernate: - ddl-auto: ${db.ddl-auto} + ddl-auto: none show-sql: false + rabbitmq: + listener: + simple: + concurrency: 1 + max-concurrency: 20 + retry: + max-attempts: 3 + enabled: true + initial-interval: 1000 + max-interval: 8000 + multiplier: 2 + default-requeue-rejected: false + dynamic: ${rabbitmq.auto-create} + host: ${rabbitmq.host} + port: ${rabbitmq.port} + username: ${rabbitmq.username} + password: ${rabbitmq.password} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 077578b8..60ec077c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,2 @@ -rootProject.name = 'drive' -include 'domain' -include 'application' -include 'infrastructure' \ No newline at end of file +rootProject.name = 'drive-api' +include('domain', 'application', 'infrastructure', 'aop') \ No newline at end of file