From f8080b8e6eb1a45e75a4537f1568c5a711864c60 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Mon, 22 Jun 2026 14:56:48 +0100 Subject: [PATCH 01/51] Prepare structure for (local) RabbitMQ --- build.gradle | 3 ++ .../ports/NotificationCompletedListener.java | 7 +++ .../ports/NotificationRequestPublisher.java | 7 +++ .../infrastructure/config/RabbitConfig.java | 43 +++++++++++++++++++ .../RabbitNotificationCompletedListener.java | 12 ++++++ .../RabbitNotificationRequestPublisher.java | 23 ++++++++++ src/main/resources/application.properties | 6 +++ 7 files changed, 101 insertions(+) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/RabbitConfig.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java diff --git a/build.gradle b/build.gradle index 01e8ef4..119098c 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ configurations { repositories { mavenCentral() + mavenLocal() } ext { @@ -40,6 +41,8 @@ dependencies { implementation 'org.mapstruct:mapstruct:1.6.3' implementation 'org.mapstruct:mapstruct-processor:1.6.3' implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation "org.springframework.boot:spring-boot-starter-amqp" + implementation "com.javanauta.ts:events:1.0.0" compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java new file mode 100644 index 0000000..acc31b5 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java @@ -0,0 +1,7 @@ +package com.javanauta.ts.taskscheduler.application.ports; + +import com.javanauta.ts.events.notification.NotificationCompletedEvent; + +public interface NotificationCompletedListener { + void completed(NotificationCompletedEvent event); +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java new file mode 100644 index 0000000..f5b2792 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java @@ -0,0 +1,7 @@ +package com.javanauta.ts.taskscheduler.application.ports; + +import com.javanauta.ts.events.notification.NotificationRequestedEvent; + +public interface NotificationRequestPublisher { + void publish(NotificationRequestedEvent event); +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/RabbitConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/RabbitConfig.java new file mode 100644 index 0000000..a044864 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/RabbitConfig.java @@ -0,0 +1,43 @@ +package com.javanauta.ts.taskscheduler.infrastructure.config; + +import com.javanauta.ts.events.messaging.Exchanges; +import com.javanauta.ts.events.messaging.Queues; +import com.javanauta.ts.events.messaging.RoutingKeys; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitConfig { + + @Bean + public TopicExchange notificationExchange() { + return new TopicExchange(Exchanges.NOTIFICATION); + } + + @Bean + public Queue notificationCompletedQueue() { + return new Queue(Queues.NOTIFICATION_COMPLETED); + } + + @Bean + public Binding notificationCompletedBinding( + Queue notificationCompletedQueue, + TopicExchange notificationExchange) { + + return BindingBuilder + .bind(notificationCompletedQueue) + .to(notificationExchange) + .with(RoutingKeys.NOTIFICATION_COMPLETED); + } + + @Bean + public MessageConverter messageConverter() { + return new JacksonJsonMessageConverter(); + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java new file mode 100644 index 0000000..784e89f --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java @@ -0,0 +1,12 @@ +package com.javanauta.ts.taskscheduler.infrastructure.messaging; + +import com.javanauta.ts.events.messaging.Queues; +import com.javanauta.ts.events.notification.NotificationCompletedEvent; +import com.javanauta.ts.taskscheduler.application.ports.NotificationCompletedListener; +import org.springframework.amqp.rabbit.annotation.RabbitListener; + +@RabbitListener(queues = Queues.NOTIFICATION_COMPLETED) +public class RabbitNotificationCompletedListener implements NotificationCompletedListener { + public void completed(NotificationCompletedEvent event) { + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java new file mode 100644 index 0000000..c659acc --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java @@ -0,0 +1,23 @@ +package com.javanauta.ts.taskscheduler.infrastructure.messaging; + +import com.javanauta.ts.events.messaging.Exchanges; +import com.javanauta.ts.events.messaging.RoutingKeys; +import com.javanauta.ts.events.notification.NotificationRequestedEvent; +import com.javanauta.ts.taskscheduler.application.ports.NotificationRequestPublisher; +import lombok.AllArgsConstructor; +import org.springframework.amqp.rabbit.core.RabbitTemplate; + +@AllArgsConstructor +public class RabbitNotificationRequestPublisher implements NotificationRequestPublisher { + + private final RabbitTemplate rabbitTemplate; + + @Override + public void publish(NotificationRequestedEvent event) { + rabbitTemplate.convertAndSend( + Exchanges.NOTIFICATION, + RoutingKeys.NOTIFICATION_REQUEST, + event + ); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2d1b273..c6db99f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,3 +15,9 @@ ts.user.service.uri=${TS_USER_SERVICE_URI:http://localhost:8080/user} # Secret key used to sign and verify JWT tokens (injected via environment variables) ts.jwt.secret=${TS_JWT_SECRET} + +# AMQP configuration +spring.rabbitmq.host=localhost +spring.rabbitmq.port=5672 +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest From 069279b51190d414ab3098ba90334bc2ee5016a9 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 26 Jun 2026 14:42:32 +0100 Subject: [PATCH 02/51] Implement minimum working flow with RabbitMQ, including migrating Cron service from BFF --- .../TaskSchedulerApplication.java | 4 ++ .../ports/NotificationCompletedListener.java | 2 +- .../ports/NotificationRequestPublisher.java | 2 +- .../application/service/CronService.java | 27 ++++++++ .../application/service/TaskService.java | 66 +++++++++++++++---- .../ts/taskscheduler/domain/model/Task.java | 5 ++ .../model/enums/NotificationStatus.java | 4 +- .../RabbitNotificationCompletedListener.java | 23 ++++++- .../RabbitNotificationRequestPublisher.java | 23 ++++++- src/main/resources/application.properties | 2 + 10 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/service/CronService.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java b/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java index 328e085..fbab108 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java @@ -1,12 +1,16 @@ package com.javanauta.ts.taskscheduler; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration; import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication(exclude = MongoAutoConfiguration.class) @EnableFeignClients +@EnableScheduling +@EnableRabbit public class TaskSchedulerApplication { public static void main(String[] args) { SpringApplication.run(TaskSchedulerApplication.class, args); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java index acc31b5..501c054 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java @@ -3,5 +3,5 @@ import com.javanauta.ts.events.notification.NotificationCompletedEvent; public interface NotificationCompletedListener { - void completed(NotificationCompletedEvent event); + void handleNotificationCompleted(NotificationCompletedEvent event); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java index f5b2792..771eef6 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java @@ -3,5 +3,5 @@ import com.javanauta.ts.events.notification.NotificationRequestedEvent; public interface NotificationRequestPublisher { - void publish(NotificationRequestedEvent event); + void publishNotificationRequest(NotificationRequestedEvent event); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/CronService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/CronService.java new file mode 100644 index 0000000..4a3b5c1 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/CronService.java @@ -0,0 +1,27 @@ +package com.javanauta.ts.taskscheduler.application.service; + +import com.javanauta.ts.taskscheduler.domain.model.Task; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CronService { + private final TaskService taskService; + + @Scheduled(cron = "${ts.cron.frequency}") + public void findTasksInNextNotificationPeriod() { + long notificationPeriod = 1L; // in hours - currently hard coded but meant to be task setting + + Instant nowTime = Instant.now(); + List tasksList = taskService.findTasksByTimePeriod(nowTime, nowTime.plus(notificationPeriod, ChronoUnit.HOURS)); + tasksList.forEach(taskService::requestTaskNotification); + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 9b62067..6aab6d4 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -1,8 +1,11 @@ package com.javanauta.ts.taskscheduler.application.service; -import com.javanauta.ts.taskscheduler.application.exception.ServiceValidationException; +import com.javanauta.ts.events.notification.NotificationCompletedEvent; +import com.javanauta.ts.events.notification.NotificationRequestedEvent; +import com.javanauta.ts.events.notification.enums.NotificationResult; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; import com.javanauta.ts.taskscheduler.application.ports.CurrentUserProvider; +import com.javanauta.ts.taskscheduler.application.ports.NotificationRequestPublisher; import com.javanauta.ts.taskscheduler.domain.data.TaskData; import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; @@ -15,6 +18,7 @@ import java.time.Instant; import java.util.List; +import java.util.UUID; @Service @RequiredArgsConstructor @@ -23,6 +27,9 @@ public class TaskService { private final TaskRepository taskRepository; private final CurrentUserProvider currentUserProvider; + private final NotificationRequestPublisher notificationRequestPublisher; + + // Controller public Task createTask(TaskData taskData) { String userEmail = currentUserProvider.getEmail(); @@ -34,12 +41,6 @@ public Task createTask(TaskData taskData) { return savedTask; } - // End point to use internally. Will be refactored to be authenticated using M2M - public List findTasksByTimePeriod(Instant initialDateTime, Instant finalDateTime) { - validateTimePeriod(initialDateTime, finalDateTime); - return taskRepository.findByScheduledDateTimeBetween(initialDateTime, finalDateTime); - } - public List findTasksByUserEmail() { return taskRepository.findByUserEmail(currentUserProvider.getEmail()); } @@ -72,6 +73,50 @@ public Task updateTask(TaskData taskData, String id) { return task; } + // Cron + + public List findTasksByTimePeriod(Instant initialDateTime, Instant finalDateTime) { + return taskRepository.findByScheduledDateTimeBetween(initialDateTime, finalDateTime); + } + + public void requestTaskNotification(Task task) { + if (!task.canBeNotified()) {return;} + + String taskId = task.getId(); + + NotificationRequestedEvent event = new NotificationRequestedEvent( + UUID.randomUUID(), + Instant.now(), + taskId, + task.getName(), + task.getDescription(), + task.getScheduledDateTime(), + task.getUserEmail(), + task.getTimeZoneId().toString()); + + notificationRequestPublisher.publishNotificationRequest(event); + + task.updateStatus(NotificationStatus.DISPATCHED); + taskRepository.save(task); + } + + // Messaging Broker + + public void processTaskNotificationCompletion(NotificationCompletedEvent event) { + Task task = getTaskOrThrow(event.taskId()); + + NotificationResult result = event.result(); + + NotificationStatus taskStatus = switch (result) { + case SUCCESS -> NotificationStatus.NOTIFIED; + case RETRYABLE_FAILURE -> NotificationStatus.FAILED_RETRYABLE; + case PERMANENT_FAILURE -> NotificationStatus.FAILED; + }; + + task.updateStatus(taskStatus); + taskRepository.save(task); + } + // internal helper/validation methods private Task getTaskOrThrow(String id) { @@ -84,11 +129,4 @@ private void validateTaskOwnership(Task task) { throw new ApplicationException(ServiceExceptionCode.NO_TASK_OWNERSHIP); } } - - // To be refactored and used internally only!!! - private void validateTimePeriod(Instant initialDateTime, Instant finalDateTime) { - if (!initialDateTime.isBefore(finalDateTime)) { - throw new ServiceValidationException("Initial date time must be before final date time"); - } - } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java index 3cb9f89..50f211b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java @@ -74,6 +74,11 @@ public void update(TaskData taskData) { modificationDateTime = Instant.now(); } + public boolean canBeNotified() { + return notificationStatus == NotificationStatus.PENDING + || notificationStatus == NotificationStatus.FAILED_RETRYABLE; + } + private void validateScheduledDate() { if (scheduledDateTime.isBefore(Instant.now())) { diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java index 508c00e..8690f42 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java @@ -2,6 +2,8 @@ public enum NotificationStatus { PENDING, + DISPATCHED, NOTIFIED, - CANCELED + FAILED_RETRYABLE, + FAILED } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java index 784e89f..e5455f4 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java @@ -3,10 +3,29 @@ import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationCompletedEvent; import com.javanauta.ts.taskscheduler.application.ports.NotificationCompletedListener; +import com.javanauta.ts.taskscheduler.application.service.TaskService; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; -@RabbitListener(queues = Queues.NOTIFICATION_COMPLETED) +@Component +@AllArgsConstructor +@Slf4j public class RabbitNotificationCompletedListener implements NotificationCompletedListener { - public void completed(NotificationCompletedEvent event) { + + private final TaskService taskService; + + @RabbitListener(queues = Queues.NOTIFICATION_COMPLETED) + @Override + public void handleNotificationCompleted(NotificationCompletedEvent event) { + log.info( + "Handling NotificationCompletedEvent for Task '{}'", + event.taskId() + ); + + taskService.processTaskNotificationCompletion(event); + + log.info("Handled successfully"); } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java index c659acc..64b6a51 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java @@ -5,19 +5,36 @@ import com.javanauta.ts.events.notification.NotificationRequestedEvent; import com.javanauta.ts.taskscheduler.application.ports.NotificationRequestPublisher; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Component; +@Component @AllArgsConstructor +@Slf4j public class RabbitNotificationRequestPublisher implements NotificationRequestPublisher { private final RabbitTemplate rabbitTemplate; @Override - public void publish(NotificationRequestedEvent event) { + public void publishNotificationRequest(NotificationRequestedEvent event) { + String exchangeName = Exchanges.NOTIFICATION; + String routingKeyName = RoutingKeys.NOTIFICATION_REQUEST; + + log.info( + "Publishing NoficationRequestEvent '{}' to exchange [{}] with routing key [{}] for Task '{}'", + event.eventId(), + exchangeName, + routingKeyName, + event.taskId() + ); + rabbitTemplate.convertAndSend( - Exchanges.NOTIFICATION, - RoutingKeys.NOTIFICATION_REQUEST, + exchangeName, + routingKeyName, event ); + + log.info("Published successfully"); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c6db99f..05240ac 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,6 +5,8 @@ server.port=8081 spring.data.mongodb.database=ts_task_scheduler +ts.cron.frequency=0 */2 * * * * + management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true From 9a6089ea9e814ea9878ca7b66107f87a5b1a760c Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 26 Jun 2026 14:55:39 +0100 Subject: [PATCH 03/51] Clean up: remove end points not used anymore --- .../controller/TaskController.java | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/presentation/controller/TaskController.java index ae70515..77cbe96 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/presentation/controller/TaskController.java @@ -2,22 +2,18 @@ import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.domain.model.Task; -import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; import com.javanauta.ts.taskscheduler.presentation.dto.in.CreateTaskRequestDTO; import com.javanauta.ts.taskscheduler.presentation.dto.in.UpdateTaskRequestDTO; import com.javanauta.ts.taskscheduler.presentation.mapper.TaskMapper; import com.javanauta.ts.taskscheduler.presentation.response.SuccessResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.time.Instant; import java.util.List; @RestController @@ -43,25 +39,8 @@ public ResponseEntity createTask( return ResponseEntity.status(httpCode).body(successResponse); } - // To be removed (and used internally only) - @GetMapping("/events") - public ResponseEntity findTasksByPeriod( - @RequestParam @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant initialDateTime, - @RequestParam @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant finalDateTime) { - - List tasks = taskService.findTasksByTimePeriod(initialDateTime, finalDateTime); - - HttpStatus httpCode = HttpStatus.OK; - SuccessResponse successResponse = new SuccessResponse( - httpCode, - taskMapper.toTaskDTOList(tasks)); - - return ResponseEntity.status(httpCode).body(successResponse); - } - @GetMapping public ResponseEntity findTasksByUserEmail() { - List tasks = taskService.findTasksByUserEmail(); HttpStatus httpCode = HttpStatus.OK; @@ -80,22 +59,6 @@ public ResponseEntity deleteTask( return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } - // To be removed (will be done internally once proper asynch communication with Notifier ms is implemented) - @PatchMapping("/status") - public ResponseEntity updateTaskStatus( - @RequestParam("status") @NotNull NotificationStatus notificationStatus, - @RequestParam("id") @NotBlank String id) { - - Task task = taskService.updateTaskStatus(notificationStatus, id); - - HttpStatus httpCode = HttpStatus.OK; - SuccessResponse successResponse = new SuccessResponse( - httpCode, - taskMapper.toTaskDTO(task)); - - return ResponseEntity.status(httpCode).body(successResponse); - } - @PatchMapping public ResponseEntity updateTask( @RequestBody @Valid UpdateTaskRequestDTO taskDTO, From 94434f6503d7b11e5801c0142ce7e15659abbb18 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 26 Jun 2026 14:58:26 +0100 Subject: [PATCH 04/51] Clean up: remove exceptions not used anymore --- .../exception/ForbiddenException.java | 10 ---------- .../exception/ResourceNotFoundException.java | 8 -------- .../exception/ServiceValidationException.java | 8 -------- .../BusinessValidationException.java | 10 ---------- .../exception/GlobalExceptionHandler.java | 19 ------------------- 5 files changed, 55 deletions(-) delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/exception/ForbiddenException.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/exception/ResourceNotFoundException.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/exception/ServiceValidationException.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/domain/exception/BusinessValidationException.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ForbiddenException.java b/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ForbiddenException.java deleted file mode 100644 index 61ca59a..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ForbiddenException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.javanauta.ts.taskscheduler.application.exception; - -public class ForbiddenException extends RuntimeException { - public ForbiddenException(String message) { - super(message); - } - public ForbiddenException(String message, Throwable throwable) { - super(message, throwable); - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ResourceNotFoundException.java b/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ResourceNotFoundException.java deleted file mode 100644 index 4bf7dfd..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ResourceNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javanauta.ts.taskscheduler.application.exception; - -public class ResourceNotFoundException extends RuntimeException { - public ResourceNotFoundException(String message) { - super(message); - } - public ResourceNotFoundException(String message, Throwable throwable) { super(message, throwable); } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ServiceValidationException.java b/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ServiceValidationException.java deleted file mode 100644 index 69d09a1..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/exception/ServiceValidationException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javanauta.ts.taskscheduler.application.exception; - -public class ServiceValidationException extends RuntimeException { - public ServiceValidationException(String message) { - super(message); - } - public ServiceValidationException(String message, Throwable throwable) { super(message, throwable); } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/exception/BusinessValidationException.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/exception/BusinessValidationException.java deleted file mode 100644 index 15bbbc9..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/exception/BusinessValidationException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.javanauta.ts.taskscheduler.domain.exception; - -public class BusinessValidationException extends RuntimeException { - public BusinessValidationException(String message) { - super(message); - } - public BusinessValidationException(String message, Throwable throwable) { - super(message, throwable); - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/GlobalExceptionHandler.java b/src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/GlobalExceptionHandler.java index 4e2ba8d..39cec21 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/GlobalExceptionHandler.java @@ -1,6 +1,5 @@ package com.javanauta.ts.taskscheduler.presentation.exception; -import com.javanauta.ts.taskscheduler.application.exception.ServiceValidationException; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; import com.javanauta.ts.taskscheduler.domain.exception.enums.DomainExceptionCode; import com.javanauta.ts.taskscheduler.presentation.exception.enums.PresentationExceptionCode; @@ -193,24 +192,6 @@ public ResponseEntity handleApplicationException(ApplicationExcep return ResponseEntity.status(httpCode).body(errorResponse); } - // Service exceptions - - // To be removed as it will be used internally only - @ExceptionHandler(ServiceValidationException.class) - public ResponseEntity handlerServiceValidationException(ServiceValidationException ex) { - ExceptionCode exceptionCode = PresentationExceptionCode.INTERNAL_SERVER_ERROR; - HttpStatus httpCode = PRESENTATION_CODE_HTTP_STATUS_MAP.get(exceptionCode); - String errorIdentifier = exceptionCode.getIdentifier(); - - ErrorResponse errorResponse = new ErrorResponse( - httpCode, - errorIdentifier, - ex.getMessage(), - List.of()); - - return ResponseEntity.status(httpCode).body(errorResponse); - } - // Generic error handling @ExceptionHandler(Exception.class) From 8f6b2ad2c088fc29f64fd653115c5cf50b20752d Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 3 Jul 2026 10:03:21 +0100 Subject: [PATCH 05/51] Adjust names (and NotificationCompleted flow) based on changes in the ts:events package --- .../ports/NotificationRequestPublisher.java | 4 ++-- .../application/service/TaskService.java | 15 +++------------ .../RabbitNotificationRequestPublisher.java | 4 ++-- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java index 771eef6..0a5f04d 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java @@ -1,7 +1,7 @@ package com.javanauta.ts.taskscheduler.application.ports; -import com.javanauta.ts.events.notification.NotificationRequestedEvent; +import com.javanauta.ts.events.notification.NotificationRequestEvent; public interface NotificationRequestPublisher { - void publishNotificationRequest(NotificationRequestedEvent event); + void publishNotificationRequest(NotificationRequestEvent event); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 6aab6d4..15322e2 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -1,8 +1,7 @@ package com.javanauta.ts.taskscheduler.application.service; import com.javanauta.ts.events.notification.NotificationCompletedEvent; -import com.javanauta.ts.events.notification.NotificationRequestedEvent; -import com.javanauta.ts.events.notification.enums.NotificationResult; +import com.javanauta.ts.events.notification.NotificationRequestEvent; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; import com.javanauta.ts.taskscheduler.application.ports.CurrentUserProvider; import com.javanauta.ts.taskscheduler.application.ports.NotificationRequestPublisher; @@ -84,7 +83,7 @@ public void requestTaskNotification(Task task) { String taskId = task.getId(); - NotificationRequestedEvent event = new NotificationRequestedEvent( + NotificationRequestEvent event = new NotificationRequestEvent( UUID.randomUUID(), Instant.now(), taskId, @@ -105,15 +104,7 @@ public void requestTaskNotification(Task task) { public void processTaskNotificationCompletion(NotificationCompletedEvent event) { Task task = getTaskOrThrow(event.taskId()); - NotificationResult result = event.result(); - - NotificationStatus taskStatus = switch (result) { - case SUCCESS -> NotificationStatus.NOTIFIED; - case RETRYABLE_FAILURE -> NotificationStatus.FAILED_RETRYABLE; - case PERMANENT_FAILURE -> NotificationStatus.FAILED; - }; - - task.updateStatus(taskStatus); + task.updateStatus(NotificationStatus.NOTIFIED); taskRepository.save(task); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java index 64b6a51..89d3733 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java @@ -2,7 +2,7 @@ import com.javanauta.ts.events.messaging.Exchanges; import com.javanauta.ts.events.messaging.RoutingKeys; -import com.javanauta.ts.events.notification.NotificationRequestedEvent; +import com.javanauta.ts.events.notification.NotificationRequestEvent; import com.javanauta.ts.taskscheduler.application.ports.NotificationRequestPublisher; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,7 +17,7 @@ public class RabbitNotificationRequestPublisher implements NotificationRequestPu private final RabbitTemplate rabbitTemplate; @Override - public void publishNotificationRequest(NotificationRequestedEvent event) { + public void publishNotificationRequest(NotificationRequestEvent event) { String exchangeName = Exchanges.NOTIFICATION; String routingKeyName = RoutingKeys.NOTIFICATION_REQUEST; From e719d3e6f04de7cc3f03cf97f634a6de55326244 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 3 Jul 2026 13:14:08 +0100 Subject: [PATCH 06/51] Adapt packages structure to adapters/ports to better support different input mechanisms (REST/RabbitMQ) --- .../config/RabbitConfig.java | 2 +- .../service => adapters/in/cron}/CronService.java | 3 ++- .../messaging/RabbitNotificationCompletedListener.java | 6 ++---- .../in}/security/JwtAuthenticationEntryPoint.java | 2 +- .../in}/security/JwtRequestFilter.java | 2 +- .../in}/security/JwtUtil.java | 2 +- .../in}/security/UserDetailsServiceImpl.java | 6 +++--- .../in/security/config}/SecurityConfig.java | 3 ++- .../in/web}/controller/TaskController.java | 10 +++++----- .../in/web}/dto/in/CreateTaskRequestDTO.java | 4 ++-- .../in/web}/dto/in/UpdateTaskRequestDTO.java | 6 +++--- .../in/web}/dto/out/TaskResponseDTO.java | 2 +- .../in/web}/exception/GlobalExceptionHandler.java | 8 ++++---- .../exception/enums/PresentationExceptionCode.java | 2 +- .../in/web}/mapper/TaskMapper.java | 8 ++++---- .../in/web}/response/ErrorResponse.java | 4 ++-- .../in/web}/response/Response.java | 4 ++-- .../in/web}/response/SuccessResponse.java | 4 ++-- .../in/web}/response/ValidationErrorDetail.java | 2 +- .../adapters/in/web/response/enums/ResponseStatus.java | 6 ++++++ .../in/web}/validation/AtLeastOneField.java | 2 +- .../in/web}/validation/AtLeastOneFieldValidator.java | 2 +- .../in/web}/validation/ValidZoneId.java | 2 +- .../in/web}/validation/ValidZoneIdValidator.java | 2 +- .../out}/client/UserClient.java | 4 ++-- .../out}/client/dto/ExternalUserDTO.java | 2 +- .../messaging/RabbitNotificationRequestPublisher.java | 4 ++-- .../out/persistence}/TaskRepository.java | 2 +- .../out/persistence}/config/MongoConfig.java | 6 +++--- .../persistence/converter/ZoneIdReadConverter.java | 2 +- .../persistence/converter/ZoneIdWriteConverter.java | 2 +- .../security/SpringSecurityCurrentUserProvider.java | 4 ++-- .../ports/NotificationCompletedListener.java | 7 ------- .../taskscheduler/application/service/TaskService.java | 6 +++--- .../out/messaging}/NotificationRequestPublisher.java | 2 +- .../out/security}/CurrentUserProvider.java | 2 +- .../presentation/response/enums/ResponseStatus.java | 6 ------ 37 files changed, 68 insertions(+), 75 deletions(-) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters}/config/RabbitConfig.java (95%) rename src/main/java/com/javanauta/ts/taskscheduler/{application/service => adapters/in/cron}/CronService.java (87%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/in}/messaging/RabbitNotificationCompletedListener.java (76%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/in}/security/JwtAuthenticationEntryPoint.java (92%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/in}/security/JwtRequestFilter.java (96%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/in}/security/JwtUtil.java (93%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/in}/security/UserDetailsServiceImpl.java (76%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure/security => adapters/in/security/config}/SecurityConfig.java (92%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/controller/TaskController.java (86%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/dto/in/CreateTaskRequestDTO.java (74%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/dto/in/UpdateTaskRequestDTO.java (57%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/dto/out/TaskResponseDTO.java (89%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/exception/GlobalExceptionHandler.java (96%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/exception/enums/PresentationExceptionCode.java (93%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/mapper/TaskMapper.java (80%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/response/ErrorResponse.java (82%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/response/Response.java (67%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/response/SuccessResponse.java (77%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/response/ValidationErrorDetail.java (78%) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/enums/ResponseStatus.java rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/validation/AtLeastOneField.java (89%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/validation/AtLeastOneFieldValidator.java (92%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/validation/ValidZoneId.java (89%) rename src/main/java/com/javanauta/ts/taskscheduler/{presentation => adapters/in/web}/validation/ValidZoneIdValidator.java (89%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/out}/client/UserClient.java (76%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/out}/client/dto/ExternalUserDTO.java (72%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/out}/messaging/RabbitNotificationRequestPublisher.java (88%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure/repository => adapters/out/persistence}/TaskRepository.java (87%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/out/persistence}/config/MongoConfig.java (84%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/out}/persistence/converter/ZoneIdReadConverter.java (81%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/out}/persistence/converter/ZoneIdWriteConverter.java (81%) rename src/main/java/com/javanauta/ts/taskscheduler/{infrastructure => adapters/out}/security/SpringSecurityCurrentUserProvider.java (84%) delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java rename src/main/java/com/javanauta/ts/taskscheduler/{application/ports => ports/out/messaging}/NotificationRequestPublisher.java (76%) rename src/main/java/com/javanauta/ts/taskscheduler/{application/ports => ports/out/security}/CurrentUserProvider.java (52%) delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/presentation/response/enums/ResponseStatus.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/RabbitConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/config/RabbitConfig.java similarity index 95% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/RabbitConfig.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/config/RabbitConfig.java index a044864..753f423 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/RabbitConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/config/RabbitConfig.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.infrastructure.config; +package com.javanauta.ts.taskscheduler.adapters.config; import com.javanauta.ts.events.messaging.Exchanges; import com.javanauta.ts.events.messaging.Queues; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/CronService.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/cron/CronService.java similarity index 87% rename from src/main/java/com/javanauta/ts/taskscheduler/application/service/CronService.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/cron/CronService.java index 4a3b5c1..ad9b5ef 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/CronService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/cron/CronService.java @@ -1,5 +1,6 @@ -package com.javanauta.ts.taskscheduler.application.service; +package com.javanauta.ts.taskscheduler.adapters.in.cron; +import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.domain.model.Task; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java similarity index 76% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java index e5455f4..5116fe1 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationCompletedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java @@ -1,8 +1,7 @@ -package com.javanauta.ts.taskscheduler.infrastructure.messaging; +package com.javanauta.ts.taskscheduler.adapters.in.messaging; import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationCompletedEvent; -import com.javanauta.ts.taskscheduler.application.ports.NotificationCompletedListener; import com.javanauta.ts.taskscheduler.application.service.TaskService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,12 +11,11 @@ @Component @AllArgsConstructor @Slf4j -public class RabbitNotificationCompletedListener implements NotificationCompletedListener { +public class RabbitNotificationCompletedListener { private final TaskService taskService; @RabbitListener(queues = Queues.NOTIFICATION_COMPLETED) - @Override public void handleNotificationCompleted(NotificationCompletedEvent event) { log.info( "Handling NotificationCompletedEvent for Task '{}'", diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java similarity index 92% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtAuthenticationEntryPoint.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java index 5e21cc4..bd90f41 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.infrastructure.security; +package com.javanauta.ts.taskscheduler.adapters.in.security; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtRequestFilter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtRequestFilter.java similarity index 96% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtRequestFilter.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtRequestFilter.java index 154d6fa..2c8dcf2 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtRequestFilter.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtRequestFilter.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.infrastructure.security; +package com.javanauta.ts.taskscheduler.adapters.in.security; import feign.FeignException; import jakarta.servlet.FilterChain; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtUtil.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java similarity index 93% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtUtil.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java index 6f57f5b..687b12d 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/JwtUtil.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.infrastructure.security; +package com.javanauta.ts.taskscheduler.adapters.in.security; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java similarity index 76% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java index 9faccfc..002d924 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java @@ -1,7 +1,7 @@ -package com.javanauta.ts.taskscheduler.infrastructure.security; +package com.javanauta.ts.taskscheduler.adapters.in.security; -import com.javanauta.ts.taskscheduler.infrastructure.client.dto.ExternalUserDTO; -import com.javanauta.ts.taskscheduler.infrastructure.client.UserClient; +import com.javanauta.ts.taskscheduler.adapters.out.client.dto.ExternalUserDTO; +import com.javanauta.ts.taskscheduler.adapters.out.client.UserClient; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/SecurityConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java similarity index 92% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/SecurityConfig.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java index cbf072a..cf3ecf1 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/SecurityConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java @@ -1,5 +1,6 @@ -package com.javanauta.ts.taskscheduler.infrastructure.security; +package com.javanauta.ts.taskscheduler.adapters.in.security.config; +import com.javanauta.ts.taskscheduler.adapters.in.security.JwtRequestFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java similarity index 86% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/controller/TaskController.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java index 77cbe96..e4e1b6f 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java @@ -1,11 +1,11 @@ -package com.javanauta.ts.taskscheduler.presentation.controller; +package com.javanauta.ts.taskscheduler.adapters.in.web.controller; import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.domain.model.Task; -import com.javanauta.ts.taskscheduler.presentation.dto.in.CreateTaskRequestDTO; -import com.javanauta.ts.taskscheduler.presentation.dto.in.UpdateTaskRequestDTO; -import com.javanauta.ts.taskscheduler.presentation.mapper.TaskMapper; -import com.javanauta.ts.taskscheduler.presentation.response.SuccessResponse; +import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.CreateTaskRequestDTO; +import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.UpdateTaskRequestDTO; +import com.javanauta.ts.taskscheduler.adapters.in.web.mapper.TaskMapper; +import com.javanauta.ts.taskscheduler.adapters.in.web.response.SuccessResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/in/CreateTaskRequestDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/CreateTaskRequestDTO.java similarity index 74% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/in/CreateTaskRequestDTO.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/CreateTaskRequestDTO.java index 8a37bf3..bb38c52 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/in/CreateTaskRequestDTO.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/CreateTaskRequestDTO.java @@ -1,6 +1,6 @@ -package com.javanauta.ts.taskscheduler.presentation.dto.in; +package com.javanauta.ts.taskscheduler.adapters.in.web.dto.in; -import com.javanauta.ts.taskscheduler.presentation.validation.ValidZoneId; +import com.javanauta.ts.taskscheduler.adapters.in.web.validation.ValidZoneId; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Builder; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/in/UpdateTaskRequestDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/UpdateTaskRequestDTO.java similarity index 57% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/in/UpdateTaskRequestDTO.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/UpdateTaskRequestDTO.java index fea0c4d..ddf649a 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/in/UpdateTaskRequestDTO.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/UpdateTaskRequestDTO.java @@ -1,7 +1,7 @@ -package com.javanauta.ts.taskscheduler.presentation.dto.in; +package com.javanauta.ts.taskscheduler.adapters.in.web.dto.in; -import com.javanauta.ts.taskscheduler.presentation.validation.AtLeastOneField; -import com.javanauta.ts.taskscheduler.presentation.validation.ValidZoneId; +import com.javanauta.ts.taskscheduler.adapters.in.web.validation.AtLeastOneField; +import com.javanauta.ts.taskscheduler.adapters.in.web.validation.ValidZoneId; import lombok.Builder; import lombok.extern.jackson.Jacksonized; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/out/TaskResponseDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java similarity index 89% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/out/TaskResponseDTO.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java index 661574e..7d1bf30 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/dto/out/TaskResponseDTO.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.presentation.dto.out; +package com.javanauta.ts.taskscheduler.adapters.in.web.dto.out; import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; import lombok.*; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/GlobalExceptionHandler.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java similarity index 96% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/GlobalExceptionHandler.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java index 39cec21..ad961c8 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java @@ -1,10 +1,10 @@ -package com.javanauta.ts.taskscheduler.presentation.exception; +package com.javanauta.ts.taskscheduler.adapters.in.web.exception; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; import com.javanauta.ts.taskscheduler.domain.exception.enums.DomainExceptionCode; -import com.javanauta.ts.taskscheduler.presentation.exception.enums.PresentationExceptionCode; -import com.javanauta.ts.taskscheduler.presentation.response.ErrorResponse; -import com.javanauta.ts.taskscheduler.presentation.response.ValidationErrorDetail; +import com.javanauta.ts.taskscheduler.adapters.in.web.exception.enums.PresentationExceptionCode; +import com.javanauta.ts.taskscheduler.adapters.in.web.response.ErrorResponse; +import com.javanauta.ts.taskscheduler.adapters.in.web.response.ValidationErrorDetail; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import com.javanauta.ts.taskscheduler.shared.exception.ExceptionCode; import com.javanauta.ts.taskscheduler.shared.exception.enums.ValidationExceptionSourceType; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/enums/PresentationExceptionCode.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/enums/PresentationExceptionCode.java similarity index 93% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/enums/PresentationExceptionCode.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/enums/PresentationExceptionCode.java index 42e8daf..6a8a403 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/exception/enums/PresentationExceptionCode.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/enums/PresentationExceptionCode.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.presentation.exception.enums; +package com.javanauta.ts.taskscheduler.adapters.in.web.exception.enums; import com.javanauta.ts.taskscheduler.shared.exception.ExceptionCode; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/mapper/TaskMapper.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/mapper/TaskMapper.java similarity index 80% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/mapper/TaskMapper.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/mapper/TaskMapper.java index 49cfed3..f03d96f 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/mapper/TaskMapper.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/mapper/TaskMapper.java @@ -1,10 +1,10 @@ -package com.javanauta.ts.taskscheduler.presentation.mapper; +package com.javanauta.ts.taskscheduler.adapters.in.web.mapper; import com.javanauta.ts.taskscheduler.domain.data.TaskData; import com.javanauta.ts.taskscheduler.domain.model.Task; -import com.javanauta.ts.taskscheduler.presentation.dto.out.TaskResponseDTO; -import com.javanauta.ts.taskscheduler.presentation.dto.in.CreateTaskRequestDTO; -import com.javanauta.ts.taskscheduler.presentation.dto.in.UpdateTaskRequestDTO; +import com.javanauta.ts.taskscheduler.adapters.in.web.dto.out.TaskResponseDTO; +import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.CreateTaskRequestDTO; +import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.UpdateTaskRequestDTO; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.NullValuePropertyMappingStrategy; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/ErrorResponse.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ErrorResponse.java similarity index 82% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/response/ErrorResponse.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ErrorResponse.java index 33f455b..659edfe 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/ErrorResponse.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ErrorResponse.java @@ -1,7 +1,7 @@ -package com.javanauta.ts.taskscheduler.presentation.response; +package com.javanauta.ts.taskscheduler.adapters.in.web.response; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.javanauta.ts.taskscheduler.presentation.response.enums.ResponseStatus; +import com.javanauta.ts.taskscheduler.adapters.in.web.response.enums.ResponseStatus; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/Response.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/Response.java similarity index 67% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/response/Response.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/Response.java index 3c40bb6..6ed8a7e 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/Response.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/Response.java @@ -1,6 +1,6 @@ -package com.javanauta.ts.taskscheduler.presentation.response; +package com.javanauta.ts.taskscheduler.adapters.in.web.response; -import com.javanauta.ts.taskscheduler.presentation.response.enums.ResponseStatus; +import com.javanauta.ts.taskscheduler.adapters.in.web.response.enums.ResponseStatus; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/SuccessResponse.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/SuccessResponse.java similarity index 77% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/response/SuccessResponse.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/SuccessResponse.java index aa6a343..8501e35 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/SuccessResponse.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/SuccessResponse.java @@ -1,7 +1,7 @@ -package com.javanauta.ts.taskscheduler.presentation.response; +package com.javanauta.ts.taskscheduler.adapters.in.web.response; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.javanauta.ts.taskscheduler.presentation.response.enums.ResponseStatus; +import com.javanauta.ts.taskscheduler.adapters.in.web.response.enums.ResponseStatus; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/ValidationErrorDetail.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ValidationErrorDetail.java similarity index 78% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/response/ValidationErrorDetail.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ValidationErrorDetail.java index f2182b5..5d0c105 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/ValidationErrorDetail.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ValidationErrorDetail.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.presentation.response; +package com.javanauta.ts.taskscheduler.adapters.in.web.response; import com.fasterxml.jackson.annotation.JsonPropertyOrder; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/enums/ResponseStatus.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/enums/ResponseStatus.java new file mode 100644 index 0000000..686d1b9 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/enums/ResponseStatus.java @@ -0,0 +1,6 @@ +package com.javanauta.ts.taskscheduler.adapters.in.web.response.enums; + +public enum ResponseStatus { + SUCCESS, + ERROR +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/AtLeastOneField.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/AtLeastOneField.java similarity index 89% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/AtLeastOneField.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/AtLeastOneField.java index d906342..4d3919b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/AtLeastOneField.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/AtLeastOneField.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.presentation.validation; +package com.javanauta.ts.taskscheduler.adapters.in.web.validation; import jakarta.validation.Constraint; import jakarta.validation.Payload; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/AtLeastOneFieldValidator.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/AtLeastOneFieldValidator.java similarity index 92% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/AtLeastOneFieldValidator.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/AtLeastOneFieldValidator.java index 5125b90..1b62a1b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/AtLeastOneFieldValidator.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/AtLeastOneFieldValidator.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.presentation.validation; +package com.javanauta.ts.taskscheduler.adapters.in.web.validation; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/ValidZoneId.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/ValidZoneId.java similarity index 89% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/ValidZoneId.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/ValidZoneId.java index 7625a0a..54dd32a 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/ValidZoneId.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/ValidZoneId.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.presentation.validation; +package com.javanauta.ts.taskscheduler.adapters.in.web.validation; import jakarta.validation.Constraint; import jakarta.validation.Payload; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/ValidZoneIdValidator.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/ValidZoneIdValidator.java similarity index 89% rename from src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/ValidZoneIdValidator.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/ValidZoneIdValidator.java index 572f163..e490943 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/validation/ValidZoneIdValidator.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/validation/ValidZoneIdValidator.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.presentation.validation; +package com.javanauta.ts.taskscheduler.adapters.in.web.validation; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/client/UserClient.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/UserClient.java similarity index 76% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/client/UserClient.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/UserClient.java index e2dba27..cc39695 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/client/UserClient.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/UserClient.java @@ -1,6 +1,6 @@ -package com.javanauta.ts.taskscheduler.infrastructure.client; +package com.javanauta.ts.taskscheduler.adapters.out.client; -import com.javanauta.ts.taskscheduler.infrastructure.client.dto.ExternalUserDTO; +import com.javanauta.ts.taskscheduler.adapters.out.client.dto.ExternalUserDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/client/dto/ExternalUserDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/dto/ExternalUserDTO.java similarity index 72% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/client/dto/ExternalUserDTO.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/dto/ExternalUserDTO.java index 1cbc7d2..2c32d12 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/client/dto/ExternalUserDTO.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/dto/ExternalUserDTO.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.infrastructure.client.dto; +package com.javanauta.ts.taskscheduler.adapters.out.client.dto; import lombok.*; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java similarity index 88% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java index 89d3733..b3021d2 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/messaging/RabbitNotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java @@ -1,9 +1,9 @@ -package com.javanauta.ts.taskscheduler.infrastructure.messaging; +package com.javanauta.ts.taskscheduler.adapters.out.messaging; import com.javanauta.ts.events.messaging.Exchanges; import com.javanauta.ts.events.messaging.RoutingKeys; import com.javanauta.ts.events.notification.NotificationRequestEvent; -import com.javanauta.ts.taskscheduler.application.ports.NotificationRequestPublisher; +import com.javanauta.ts.taskscheduler.ports.out.messaging.NotificationRequestPublisher; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/repository/TaskRepository.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskRepository.java similarity index 87% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/repository/TaskRepository.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskRepository.java index 06ed8d7..c1756c1 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/repository/TaskRepository.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskRepository.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.infrastructure.repository; +package com.javanauta.ts.taskscheduler.adapters.out.persistence; import com.javanauta.ts.taskscheduler.domain.model.Task; import org.springframework.data.mongodb.repository.MongoRepository; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/MongoConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java similarity index 84% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/MongoConfig.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java index c8730a1..4b81e98 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/config/MongoConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java @@ -1,7 +1,7 @@ -package com.javanauta.ts.taskscheduler.infrastructure.config; +package com.javanauta.ts.taskscheduler.adapters.out.persistence.config; -import com.javanauta.ts.taskscheduler.infrastructure.persistence.converter.ZoneIdReadConverter; -import com.javanauta.ts.taskscheduler.infrastructure.persistence.converter.ZoneIdWriteConverter; +import com.javanauta.ts.taskscheduler.adapters.out.persistence.converter.ZoneIdReadConverter; +import com.javanauta.ts.taskscheduler.adapters.out.persistence.converter.ZoneIdWriteConverter; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/persistence/converter/ZoneIdReadConverter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/converter/ZoneIdReadConverter.java similarity index 81% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/persistence/converter/ZoneIdReadConverter.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/converter/ZoneIdReadConverter.java index 8e5849b..03ea92e 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/persistence/converter/ZoneIdReadConverter.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/converter/ZoneIdReadConverter.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.infrastructure.persistence.converter; +package com.javanauta.ts.taskscheduler.adapters.out.persistence.converter; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ReadingConverter; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/persistence/converter/ZoneIdWriteConverter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/converter/ZoneIdWriteConverter.java similarity index 81% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/persistence/converter/ZoneIdWriteConverter.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/converter/ZoneIdWriteConverter.java index 4a3465c..0d67b7d 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/persistence/converter/ZoneIdWriteConverter.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/converter/ZoneIdWriteConverter.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.infrastructure.persistence.converter; +package com.javanauta.ts.taskscheduler.adapters.out.persistence.converter; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.WritingConverter; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/SpringSecurityCurrentUserProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityCurrentUserProvider.java similarity index 84% rename from src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/SpringSecurityCurrentUserProvider.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityCurrentUserProvider.java index 79e1eb2..4c2bcaa 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/infrastructure/security/SpringSecurityCurrentUserProvider.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityCurrentUserProvider.java @@ -1,6 +1,6 @@ -package com.javanauta.ts.taskscheduler.infrastructure.security; +package com.javanauta.ts.taskscheduler.adapters.out.security; -import com.javanauta.ts.taskscheduler.application.ports.CurrentUserProvider; +import com.javanauta.ts.taskscheduler.ports.out.security.CurrentUserProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java deleted file mode 100644 index 501c054..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationCompletedListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.javanauta.ts.taskscheduler.application.ports; - -import com.javanauta.ts.events.notification.NotificationCompletedEvent; - -public interface NotificationCompletedListener { - void handleNotificationCompleted(NotificationCompletedEvent event); -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 15322e2..c8f5dc2 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -3,12 +3,12 @@ import com.javanauta.ts.events.notification.NotificationCompletedEvent; import com.javanauta.ts.events.notification.NotificationRequestEvent; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; -import com.javanauta.ts.taskscheduler.application.ports.CurrentUserProvider; -import com.javanauta.ts.taskscheduler.application.ports.NotificationRequestPublisher; +import com.javanauta.ts.taskscheduler.ports.out.security.CurrentUserProvider; +import com.javanauta.ts.taskscheduler.ports.out.messaging.NotificationRequestPublisher; import com.javanauta.ts.taskscheduler.domain.data.TaskData; import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; -import com.javanauta.ts.taskscheduler.infrastructure.repository.TaskRepository; +import com.javanauta.ts.taskscheduler.adapters.out.persistence.TaskRepository; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/messaging/NotificationRequestPublisher.java similarity index 76% rename from src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java rename to src/main/java/com/javanauta/ts/taskscheduler/ports/out/messaging/NotificationRequestPublisher.java index 0a5f04d..ad6242b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/NotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/messaging/NotificationRequestPublisher.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.application.ports; +package com.javanauta.ts.taskscheduler.ports.out.messaging; import com.javanauta.ts.events.notification.NotificationRequestEvent; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/CurrentUserProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/CurrentUserProvider.java similarity index 52% rename from src/main/java/com/javanauta/ts/taskscheduler/application/ports/CurrentUserProvider.java rename to src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/CurrentUserProvider.java index 8755216..a418e95 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/ports/CurrentUserProvider.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/CurrentUserProvider.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.application.ports; +package com.javanauta.ts.taskscheduler.ports.out.security; public interface CurrentUserProvider { String getEmail(); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/enums/ResponseStatus.java b/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/enums/ResponseStatus.java deleted file mode 100644 index 1ef456c..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/presentation/response/enums/ResponseStatus.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.javanauta.ts.taskscheduler.presentation.response.enums; - -public enum ResponseStatus { - SUCCESS, - ERROR -} From d97a0d66b0cb720ee353d44773c1789c2bdf3813 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 3 Jul 2026 14:39:39 +0100 Subject: [PATCH 07/51] Add some missing output port interfaces, adjust some Spring stereotypes and rename some packages and classes --- .../messaging/config/RabbitInConfig.java} | 11 +---- .../TaskNotificationTrigger.java} | 10 ++--- .../adapters/in/security/JwtUtil.java | 4 +- .../in/security/UserDetailsServiceImpl.java | 16 ++++---- .../user/ExternalUserProviderAdapter.java | 18 ++++++++ .../FeignUserClient.java} | 6 +-- .../{ => user}/dto/ExternalUserDTO.java | 3 +- ...pository.java => MongoTaskRepository.java} | 2 +- .../out/persistence/TaskPersisterAdapter.java | 41 +++++++++++++++++++ ...a => SpringSecurityPrincipalProvider.java} | 4 +- .../shared/config/RabbitCommonConfig.java | 15 +++++++ .../application/service/TaskService.java | 26 ++++++------ .../out/client/user/ExternalUserProvider.java | 7 ++++ .../ports/out/persistence/TaskPersister.java | 15 +++++++ ...erProvider.java => PrincipalProvider.java} | 2 +- src/main/resources/application.properties | 2 +- 16 files changed, 135 insertions(+), 47 deletions(-) rename src/main/java/com/javanauta/ts/taskscheduler/adapters/{config/RabbitConfig.java => in/messaging/config/RabbitInConfig.java} (75%) rename src/main/java/com/javanauta/ts/taskscheduler/adapters/in/{cron/CronService.java => scheduler/TaskNotificationTrigger.java} (79%) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/ExternalUserProviderAdapter.java rename src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/{UserClient.java => user/FeignUserClient.java} (70%) rename src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/{ => user}/dto/ExternalUserDTO.java (71%) rename src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/{TaskRepository.java => MongoTaskRepository.java} (85%) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java rename src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/{SpringSecurityCurrentUserProvider.java => SpringSecurityPrincipalProvider.java} (82%) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/shared/config/RabbitCommonConfig.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/ports/out/client/user/ExternalUserProvider.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java rename src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/{CurrentUserProvider.java => PrincipalProvider.java} (68%) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/config/RabbitConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java similarity index 75% rename from src/main/java/com/javanauta/ts/taskscheduler/adapters/config/RabbitConfig.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java index 753f423..02106ec 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/config/RabbitConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.adapters.config; +package com.javanauta.ts.taskscheduler.adapters.in.messaging.config; import com.javanauta.ts.events.messaging.Exchanges; import com.javanauta.ts.events.messaging.Queues; @@ -7,13 +7,11 @@ import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.TopicExchange; -import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; -import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -public class RabbitConfig { +public class RabbitInConfig { @Bean public TopicExchange notificationExchange() { @@ -35,9 +33,4 @@ public Binding notificationCompletedBinding( .to(notificationExchange) .with(RoutingKeys.NOTIFICATION_COMPLETED); } - - @Bean - public MessageConverter messageConverter() { - return new JacksonJsonMessageConverter(); - } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/cron/CronService.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/scheduler/TaskNotificationTrigger.java similarity index 79% rename from src/main/java/com/javanauta/ts/taskscheduler/adapters/in/cron/CronService.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/scheduler/TaskNotificationTrigger.java index ad9b5ef..3888c7e 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/cron/CronService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/scheduler/TaskNotificationTrigger.java @@ -1,23 +1,23 @@ -package com.javanauta.ts.taskscheduler.adapters.in.cron; +package com.javanauta.ts.taskscheduler.adapters.in.scheduler; import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.domain.model.Task; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; -@Service +@Component @RequiredArgsConstructor @Slf4j -public class CronService { +public class TaskNotificationTrigger { private final TaskService taskService; - @Scheduled(cron = "${ts.cron.frequency}") + @Scheduled(cron = "${ts.scheduler.frequency}") public void findTasksInNextNotificationPeriod() { long notificationPeriod = 1L; // in hours - currently hard coded but meant to be task setting diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java index 687b12d..3bed5d3 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java @@ -6,12 +6,12 @@ import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.util.Date; -@Service +@Component public class JwtUtil { private JwtParser jwtParser; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java index 002d924..34139a1 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java @@ -1,21 +1,21 @@ package com.javanauta.ts.taskscheduler.adapters.in.security; -import com.javanauta.ts.taskscheduler.adapters.out.client.dto.ExternalUserDTO; -import com.javanauta.ts.taskscheduler.adapters.out.client.UserClient; +import com.javanauta.ts.taskscheduler.adapters.out.client.user.dto.ExternalUserDTO; +import com.javanauta.ts.taskscheduler.ports.out.client.user.ExternalUserProvider; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; -@Service +@Component public class UserDetailsServiceImpl { - private final UserClient userClient; + private final ExternalUserProvider externalUserProvider; - public UserDetailsServiceImpl(UserClient userClient) { - this.userClient = userClient; + public UserDetailsServiceImpl(ExternalUserProvider externalUserProvider) { + this.externalUserProvider = externalUserProvider; } public UserDetails loadUserDetails(String email, String token) { - ExternalUserDTO externalUserDTO = userClient.getUserByEmail(email, token); + ExternalUserDTO externalUserDTO = externalUserProvider.getUserByEmail(email, token); return org.springframework.security.core.userdetails.User .withUsername(externalUserDTO.getEmail()) // Sets the username as the email diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/ExternalUserProviderAdapter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/ExternalUserProviderAdapter.java new file mode 100644 index 0000000..2bc3e7f --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/ExternalUserProviderAdapter.java @@ -0,0 +1,18 @@ +package com.javanauta.ts.taskscheduler.adapters.out.client.user; + +import com.javanauta.ts.taskscheduler.adapters.out.client.user.dto.ExternalUserDTO; +import com.javanauta.ts.taskscheduler.ports.out.client.user.ExternalUserProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ExternalUserProviderAdapter implements ExternalUserProvider { + private final FeignUserClient userProvider; + + @Override + public ExternalUserDTO getUserByEmail(String email, String token) { + return userProvider.getUserByEmail(email, token); + } +} + diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/UserClient.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/FeignUserClient.java similarity index 70% rename from src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/UserClient.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/FeignUserClient.java index cc39695..4c430f6 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/UserClient.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/FeignUserClient.java @@ -1,13 +1,13 @@ -package com.javanauta.ts.taskscheduler.adapters.out.client; +package com.javanauta.ts.taskscheduler.adapters.out.client.user; -import com.javanauta.ts.taskscheduler.adapters.out.client.dto.ExternalUserDTO; +import com.javanauta.ts.taskscheduler.adapters.out.client.user.dto.ExternalUserDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(name = "user", url = "${ts.user.service.uri}") -public interface UserClient { +public interface FeignUserClient { @GetMapping ExternalUserDTO getUserByEmail(@RequestParam("email") String email, @RequestHeader("Authorization") String token); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/dto/ExternalUserDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/dto/ExternalUserDTO.java similarity index 71% rename from src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/dto/ExternalUserDTO.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/dto/ExternalUserDTO.java index 2c32d12..757debd 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/dto/ExternalUserDTO.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/dto/ExternalUserDTO.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.adapters.out.client.dto; +package com.javanauta.ts.taskscheduler.adapters.out.client.user.dto; import lombok.*; @@ -8,7 +8,6 @@ @AllArgsConstructor @Builder public class ExternalUserDTO { - private String email; private String password; } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskRepository.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java similarity index 85% rename from src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskRepository.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java index c1756c1..568760a 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskRepository.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java @@ -8,7 +8,7 @@ import java.util.List; @Repository -public interface TaskRepository extends MongoRepository { +public interface MongoTaskRepository extends MongoRepository { List findByScheduledDateTimeBetween(Instant initialDateTime, Instant finalDateTime); List findByUserEmail(String userEmail); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java new file mode 100644 index 0000000..0bd884b --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java @@ -0,0 +1,41 @@ +package com.javanauta.ts.taskscheduler.adapters.out.persistence; + +import com.javanauta.ts.taskscheduler.domain.model.Task; +import com.javanauta.ts.taskscheduler.ports.out.persistence.TaskPersister; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class TaskPersisterAdapter implements TaskPersister { + private final MongoTaskRepository taskRepository; + + @Override + public Task save(Task task) { + return taskRepository.save(task); + } + + @Override + public void deleteById(String id) { + taskRepository.deleteById(id); + } + + @Override + public Optional findById(String id) { + return taskRepository.findById(id); + } + + @Override + public List findByScheduledDateTimeBetween(Instant initialDateTime, Instant finalDateTime) { + return taskRepository.findByScheduledDateTimeBetween(initialDateTime, finalDateTime); + } + + @Override + public List findByUserEmail(String userEmail) { + return taskRepository.findByUserEmail(userEmail); + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityCurrentUserProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityPrincipalProvider.java similarity index 82% rename from src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityCurrentUserProvider.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityPrincipalProvider.java index 4c2bcaa..20f76a3 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityCurrentUserProvider.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityPrincipalProvider.java @@ -1,12 +1,12 @@ package com.javanauta.ts.taskscheduler.adapters.out.security; -import com.javanauta.ts.taskscheduler.ports.out.security.CurrentUserProvider; +import com.javanauta.ts.taskscheduler.ports.out.security.PrincipalProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @Component -public class SpringSecurityCurrentUserProvider implements CurrentUserProvider { +public class SpringSecurityPrincipalProvider implements PrincipalProvider { @Override public String getEmail() { diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/shared/config/RabbitCommonConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/shared/config/RabbitCommonConfig.java new file mode 100644 index 0000000..28d0bae --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/shared/config/RabbitCommonConfig.java @@ -0,0 +1,15 @@ +package com.javanauta.ts.taskscheduler.adapters.shared.config; + +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitCommonConfig { + + @Bean + public MessageConverter messageConverter() { + return new JacksonJsonMessageConverter(); + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index c8f5dc2..9c6e249 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -3,12 +3,12 @@ import com.javanauta.ts.events.notification.NotificationCompletedEvent; import com.javanauta.ts.events.notification.NotificationRequestEvent; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; -import com.javanauta.ts.taskscheduler.ports.out.security.CurrentUserProvider; +import com.javanauta.ts.taskscheduler.ports.out.persistence.TaskPersister; +import com.javanauta.ts.taskscheduler.ports.out.security.PrincipalProvider; import com.javanauta.ts.taskscheduler.ports.out.messaging.NotificationRequestPublisher; import com.javanauta.ts.taskscheduler.domain.data.TaskData; import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; -import com.javanauta.ts.taskscheduler.adapters.out.persistence.TaskRepository; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,30 +24,30 @@ @Slf4j public class TaskService { - private final TaskRepository taskRepository; - private final CurrentUserProvider currentUserProvider; + private final TaskPersister taskPersister; + private final PrincipalProvider principalProvider; private final NotificationRequestPublisher notificationRequestPublisher; // Controller public Task createTask(TaskData taskData) { - String userEmail = currentUserProvider.getEmail(); + String userEmail = principalProvider.getEmail(); Task task = Task.create(taskData, userEmail); - Task savedTask = taskRepository.save(task); + Task savedTask = taskPersister.save(task); log.info("Task {} created", savedTask.getId()); return savedTask; } public List findTasksByUserEmail() { - return taskRepository.findByUserEmail(currentUserProvider.getEmail()); + return taskPersister.findByUserEmail(principalProvider.getEmail()); } public void deleteTask(String id) { Task task = getTaskOrThrow(id); validateTaskOwnership(task); - taskRepository.deleteById(id); + taskPersister.deleteById(id); log.info("Task {} deleted", id); } @@ -75,7 +75,7 @@ public Task updateTask(TaskData taskData, String id) { // Cron public List findTasksByTimePeriod(Instant initialDateTime, Instant finalDateTime) { - return taskRepository.findByScheduledDateTimeBetween(initialDateTime, finalDateTime); + return taskPersister.findByScheduledDateTimeBetween(initialDateTime, finalDateTime); } public void requestTaskNotification(Task task) { @@ -96,7 +96,7 @@ public void requestTaskNotification(Task task) { notificationRequestPublisher.publishNotificationRequest(event); task.updateStatus(NotificationStatus.DISPATCHED); - taskRepository.save(task); + taskPersister.save(task); } // Messaging Broker @@ -105,18 +105,18 @@ public void processTaskNotificationCompletion(NotificationCompletedEvent event) Task task = getTaskOrThrow(event.taskId()); task.updateStatus(NotificationStatus.NOTIFIED); - taskRepository.save(task); + taskPersister.save(task); } // internal helper/validation methods private Task getTaskOrThrow(String id) { - return taskRepository.findById(id).orElseThrow(() + return taskPersister.findById(id).orElseThrow(() -> new ApplicationException(ServiceExceptionCode.TASK_NOT_FOUND)); } private void validateTaskOwnership(Task task) { - if (!task.getUserEmail().equals(currentUserProvider.getEmail())) { + if (!task.getUserEmail().equals(principalProvider.getEmail())) { throw new ApplicationException(ServiceExceptionCode.NO_TASK_OWNERSHIP); } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/client/user/ExternalUserProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/client/user/ExternalUserProvider.java new file mode 100644 index 0000000..9dbd6c1 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/client/user/ExternalUserProvider.java @@ -0,0 +1,7 @@ +package com.javanauta.ts.taskscheduler.ports.out.client.user; + +import com.javanauta.ts.taskscheduler.adapters.out.client.user.dto.ExternalUserDTO; + +public interface ExternalUserProvider { + ExternalUserDTO getUserByEmail(String email, String token); +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java new file mode 100644 index 0000000..40d221c --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java @@ -0,0 +1,15 @@ +package com.javanauta.ts.taskscheduler.ports.out.persistence; + +import com.javanauta.ts.taskscheduler.domain.model.Task; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +public interface TaskPersister { + Task save(Task task); + void deleteById(String id); + Optional findById(String id); + List findByScheduledDateTimeBetween(Instant initialDateTime, Instant finalDateTime); + List findByUserEmail(String userEmail); +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/CurrentUserProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java similarity index 68% rename from src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/CurrentUserProvider.java rename to src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java index a418e95..90da7a4 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/CurrentUserProvider.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java @@ -1,5 +1,5 @@ package com.javanauta.ts.taskscheduler.ports.out.security; -public interface CurrentUserProvider { +public interface PrincipalProvider { String getEmail(); } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 05240ac..d4a929d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,7 +5,7 @@ server.port=8081 spring.data.mongodb.database=ts_task_scheduler -ts.cron.frequency=0 */2 * * * * +ts.scheduler.frequency=0 */2 * * * * management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true From 19265dade1ba24d2e2c40bd7352f856c46a02b9e Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sat, 4 Jul 2026 14:18:36 +0100 Subject: [PATCH 08/51] Change NotifcationStatus for when failure is retryable --- .../java/com/javanauta/ts/taskscheduler/domain/model/Task.java | 2 +- .../ts/taskscheduler/domain/model/enums/NotificationStatus.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java index 50f211b..fb5c40b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java @@ -76,7 +76,7 @@ public void update(TaskData taskData) { public boolean canBeNotified() { return notificationStatus == NotificationStatus.PENDING - || notificationStatus == NotificationStatus.FAILED_RETRYABLE; + || notificationStatus == NotificationStatus.PENDING_RETRY; } private void validateScheduledDate() { diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java index 8690f42..29231ea 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java @@ -4,6 +4,6 @@ public enum NotificationStatus { PENDING, DISPATCHED, NOTIFIED, - FAILED_RETRYABLE, + PENDING_RETRY, FAILED } From e9b0d6139ff760276e97e6e72ae993e4a7e7a1ed Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sat, 4 Jul 2026 20:37:47 +0100 Subject: [PATCH 09/51] Add RabbitListener to NotificationFailed event and adjust Service accordingly --- .../RabbitNotificationCompletedListener.java | 7 --- .../RabbitNotificationFailedListener.java | 22 +++++++++ .../in/messaging/config/RabbitInConfig.java | 20 ++++++++ .../application/service/TaskService.java | 49 ++++++++++++------- 4 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java index 5116fe1..847fd89 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java @@ -17,13 +17,6 @@ public class RabbitNotificationCompletedListener { @RabbitListener(queues = Queues.NOTIFICATION_COMPLETED) public void handleNotificationCompleted(NotificationCompletedEvent event) { - log.info( - "Handling NotificationCompletedEvent for Task '{}'", - event.taskId() - ); - taskService.processTaskNotificationCompletion(event); - - log.info("Handled successfully"); } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java new file mode 100644 index 0000000..7b23187 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java @@ -0,0 +1,22 @@ +package com.javanauta.ts.taskscheduler.adapters.in.messaging; + +import com.javanauta.ts.events.messaging.Queues; +import com.javanauta.ts.events.notification.NotificationFailedEvent; +import com.javanauta.ts.taskscheduler.application.service.TaskService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RabbitNotificationFailedListener { + + private final TaskService taskService; + + @RabbitListener(queues = Queues.NOTIFICATION_FAILED) + public void handleNotificationFailed(NotificationFailedEvent event) { + taskService.processTaskNotificationFailure(event); + } +} \ No newline at end of file diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java index 02106ec..25dde13 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java @@ -3,10 +3,16 @@ import com.javanauta.ts.events.messaging.Exchanges; import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.messaging.RoutingKeys; +import org.aopalliance.aop.Advice; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -33,4 +39,18 @@ public Binding notificationCompletedBinding( .to(notificationExchange) .with(RoutingKeys.NOTIFICATION_COMPLETED); } + + @Bean + public Queue notificationFailedQueue() { return new Queue(Queues.NOTIFICATION_FAILED); } + + @Bean + public Binding notificationFailedBinding( + Queue notificationFailedQueue, + TopicExchange notificationExchange) { + + return BindingBuilder + .bind(notificationFailedQueue) + .to(notificationExchange) + .with(RoutingKeys.NOTIFICATION_FAILED); + } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 9c6e249..9cc73a8 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -1,6 +1,7 @@ package com.javanauta.ts.taskscheduler.application.service; import com.javanauta.ts.events.notification.NotificationCompletedEvent; +import com.javanauta.ts.events.notification.NotificationFailedEvent; import com.javanauta.ts.events.notification.NotificationRequestEvent; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; import com.javanauta.ts.taskscheduler.ports.out.persistence.TaskPersister; @@ -15,6 +16,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.management.Notification; import java.time.Instant; import java.util.List; import java.util.UUID; @@ -52,16 +54,6 @@ public void deleteTask(String id) { log.info("Task {} deleted", id); } - @Transactional - public Task updateTaskStatus(NotificationStatus notificationStatus, String id) { - Task task = getTaskOrThrow(id); - validateTaskOwnership(task); - task.updateStatus(notificationStatus); - - log.info("Status of task {} updated", id); - return task; - } - @Transactional public Task updateTask(TaskData taskData, String id) { Task task = getTaskOrThrow(id); @@ -72,21 +64,21 @@ public Task updateTask(TaskData taskData, String id) { return task; } - // Cron + // Scheduler public List findTasksByTimePeriod(Instant initialDateTime, Instant finalDateTime) { return taskPersister.findByScheduledDateTimeBetween(initialDateTime, finalDateTime); } public void requestTaskNotification(Task task) { - if (!task.canBeNotified()) {return;} - - String taskId = task.getId(); + if (!task.canBeNotified()) { + return; + } NotificationRequestEvent event = new NotificationRequestEvent( UUID.randomUUID(), Instant.now(), - taskId, + task.getId(), task.getName(), task.getDescription(), task.getScheduledDateTime(), @@ -94,20 +86,32 @@ public void requestTaskNotification(Task task) { task.getTimeZoneId().toString()); notificationRequestPublisher.publishNotificationRequest(event); + updateTaskStatus(task, NotificationStatus.DISPATCHED); - task.updateStatus(NotificationStatus.DISPATCHED); - taskPersister.save(task); + log.info("Notification request for Task '{}' was successfully dispatched", task.getId()); } // Messaging Broker public void processTaskNotificationCompletion(NotificationCompletedEvent event) { Task task = getTaskOrThrow(event.taskId()); + updateTaskStatus(task, NotificationStatus.NOTIFIED); - task.updateStatus(NotificationStatus.NOTIFIED); - taskPersister.save(task); + log.info("Notification for Task '{}' was successfully completed", event.taskId()); } + public void processTaskNotificationFailure(NotificationFailedEvent event) { + log.info("Notification for Task '{}' failed ({})", event.taskId(), event.failureType().name().toLowerCase()); + Task task = getTaskOrThrow(event.taskId()); + + NotificationStatus newStatus = switch (event.failureType()) { + case TEMPORARY -> NotificationStatus.PENDING_RETRY; + case PERMANENT -> NotificationStatus.FAILED; + }; + + updateTaskStatus(task, newStatus); + } + // internal helper/validation methods private Task getTaskOrThrow(String id) { @@ -120,4 +124,11 @@ private void validateTaskOwnership(Task task) { throw new ApplicationException(ServiceExceptionCode.NO_TASK_OWNERSHIP); } } + + private void updateTaskStatus(Task task, NotificationStatus status) { + task.updateStatus(status); + taskPersister.save(task); + + log.info("Status of task {} updated to {}", task.getId(), status); + } } From 8ad71892e206a1d99c89a37bf83efeff51322572 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sat, 4 Jul 2026 21:57:56 +0100 Subject: [PATCH 10/51] Fully working RabbitMQ messaging flow --- .../RabbitNotificationCompletedListener.java | 10 ++++++- .../RabbitNotificationFailedListener.java | 9 ++++++- .../in/messaging/config/RabbitInConfig.java | 26 +++++++++++++++++++ .../RabbitNotificationRequestPublisher.java | 17 ++---------- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java index 847fd89..9ed9b18 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java @@ -3,8 +3,10 @@ import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationCompletedEvent; import com.javanauta.ts.taskscheduler.application.service.TaskService; +import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @@ -17,6 +19,12 @@ public class RabbitNotificationCompletedListener { @RabbitListener(queues = Queues.NOTIFICATION_COMPLETED) public void handleNotificationCompleted(NotificationCompletedEvent event) { - taskService.processTaskNotificationCompletion(event); + try { + taskService.processTaskNotificationCompletion(event); + } catch ( + ApplicationException ex) { + // No retryable exceptions. If any exception is thrown, is probably database related and could be recovered + throw new AmqpRejectAndDontRequeueException(ex); + } } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java index 7b23187..e58f334 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java @@ -3,8 +3,10 @@ import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationFailedEvent; import com.javanauta.ts.taskscheduler.application.service.TaskService; +import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @@ -17,6 +19,11 @@ public class RabbitNotificationFailedListener { @RabbitListener(queues = Queues.NOTIFICATION_FAILED) public void handleNotificationFailed(NotificationFailedEvent event) { - taskService.processTaskNotificationFailure(event); + try { + taskService.processTaskNotificationFailure(event); + } catch (ApplicationException ex) { + // No retryable exceptions. If any exception is thrown, is probably database related and could be recovered + throw new AmqpRejectAndDontRequeueException(ex); + } } } \ No newline at end of file diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java index 25dde13..8643dd0 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java @@ -53,4 +53,30 @@ public Binding notificationFailedBinding( .to(notificationExchange) .with(RoutingKeys.NOTIFICATION_FAILED); } + + @Bean + public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( + ConnectionFactory connectionFactory, + MessageConverter messageConverter, + Advice retryAdvice) { + + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + + factory.setConnectionFactory(connectionFactory); + factory.setMessageConverter(messageConverter); + + factory.setAdviceChain(retryAdvice); + + return factory; + } + + @Bean + public Advice retryAdvice() { + return RetryInterceptorBuilder.stateless() + .configureRetryPolicy(builder -> builder + .maxRetries(3) + .excludes(AmqpRejectAndDontRequeueException.class)) + .backOffOptions(60000, 1.0, 60000) + .build(); + } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java index b3021d2..1b4ea87 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java @@ -18,23 +18,10 @@ public class RabbitNotificationRequestPublisher implements NotificationRequestPu @Override public void publishNotificationRequest(NotificationRequestEvent event) { - String exchangeName = Exchanges.NOTIFICATION; - String routingKeyName = RoutingKeys.NOTIFICATION_REQUEST; - - log.info( - "Publishing NoficationRequestEvent '{}' to exchange [{}] with routing key [{}] for Task '{}'", - event.eventId(), - exchangeName, - routingKeyName, - event.taskId() - ); - rabbitTemplate.convertAndSend( - exchangeName, - routingKeyName, + Exchanges.NOTIFICATION, + RoutingKeys.NOTIFICATION_REQUEST, event ); - - log.info("Published successfully"); } } From c4f5fb12cae904a1f610668ef2b955c33e28ffab Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Wed, 8 Jul 2026 22:17:04 +0100 Subject: [PATCH 11/51] Change creation of event to use its 'create' static method (which internally inits the eventId and occurredAt attributes) --- .../ts/taskscheduler/application/service/TaskService.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 9cc73a8..df59d75 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -75,9 +75,7 @@ public void requestTaskNotification(Task task) { return; } - NotificationRequestEvent event = new NotificationRequestEvent( - UUID.randomUUID(), - Instant.now(), + NotificationRequestEvent event = NotificationRequestEvent.create( task.getId(), task.getName(), task.getDescription(), @@ -110,7 +108,7 @@ public void processTaskNotificationFailure(NotificationFailedEvent event) { }; updateTaskStatus(task, newStatus); - } + } // internal helper/validation methods From 3ef045b52a332d2755f3399c49bb0b0fd338df28 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 9 Jul 2026 11:40:11 +0100 Subject: [PATCH 12/51] Move translation of RabbitMQ events from service to RabbitMQ Publishers/Consumers --- .../RabbitNotificationCompletedListener.java | 9 +++- .../RabbitNotificationFailedListener.java | 15 +++++- .../RabbitNotificationRequestPublisher.java | 11 ++++- .../data/NotificationResultDetails.java | 10 ++++ .../data/enums/NotificationResult.java | 7 +++ .../application/service/TaskService.java | 46 ++++++++----------- .../NotificationRequestPublisher.java | 4 +- 7 files changed, 71 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/data/NotificationResultDetails.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/application/data/enums/NotificationResult.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java index 9ed9b18..673849b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java @@ -2,6 +2,8 @@ import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationCompletedEvent; +import com.javanauta.ts.taskscheduler.application.data.NotificationResultDetails; +import com.javanauta.ts.taskscheduler.application.data.enums.NotificationResult; import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import lombok.AllArgsConstructor; @@ -19,8 +21,13 @@ public class RabbitNotificationCompletedListener { @RabbitListener(queues = Queues.NOTIFICATION_COMPLETED) public void handleNotificationCompleted(NotificationCompletedEvent event) { + NotificationResultDetails resultDetails = new NotificationResultDetails( + event.taskId(), + NotificationResult.SUCCESS, + null); + try { - taskService.processTaskNotificationCompletion(event); + taskService.processTaskNotificationCompletion(resultDetails); } catch ( ApplicationException ex) { // No retryable exceptions. If any exception is thrown, is probably database related and could be recovered diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java index e58f334..810d0cf 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java @@ -2,6 +2,8 @@ import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationFailedEvent; +import com.javanauta.ts.taskscheduler.application.data.NotificationResultDetails; +import com.javanauta.ts.taskscheduler.application.data.enums.NotificationResult; import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import lombok.RequiredArgsConstructor; @@ -19,8 +21,19 @@ public class RabbitNotificationFailedListener { @RabbitListener(queues = Queues.NOTIFICATION_FAILED) public void handleNotificationFailed(NotificationFailedEvent event) { + + NotificationResult result = switch (event.failureType()) { + case PERMANENT -> NotificationResult.PERMANENT_FAILURE; + case TEMPORARY -> NotificationResult.TEMPORARY_FAILURE; + }; + + NotificationResultDetails resultDetails = new NotificationResultDetails( + event.taskId(), + result, + event.error()); + try { - taskService.processTaskNotificationFailure(event); + taskService.processTaskNotificationFailure(resultDetails); } catch (ApplicationException ex) { // No retryable exceptions. If any exception is thrown, is probably database related and could be recovered throw new AmqpRejectAndDontRequeueException(ex); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java index 1b4ea87..70c7f6b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java @@ -3,6 +3,7 @@ import com.javanauta.ts.events.messaging.Exchanges; import com.javanauta.ts.events.messaging.RoutingKeys; import com.javanauta.ts.events.notification.NotificationRequestEvent; +import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.ports.out.messaging.NotificationRequestPublisher; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,7 +18,15 @@ public class RabbitNotificationRequestPublisher implements NotificationRequestPu private final RabbitTemplate rabbitTemplate; @Override - public void publishNotificationRequest(NotificationRequestEvent event) { + public void publishNotificationRequest(Task task) { + NotificationRequestEvent event = NotificationRequestEvent.create( + task.getId(), + task.getName(), + task.getDescription(), + task.getScheduledDateTime(), + task.getUserEmail(), + task.getTimeZoneId().toString()); + rabbitTemplate.convertAndSend( Exchanges.NOTIFICATION, RoutingKeys.NOTIFICATION_REQUEST, diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/data/NotificationResultDetails.java b/src/main/java/com/javanauta/ts/taskscheduler/application/data/NotificationResultDetails.java new file mode 100644 index 0000000..f962e6c --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/data/NotificationResultDetails.java @@ -0,0 +1,10 @@ +package com.javanauta.ts.taskscheduler.application.data; + +import com.javanauta.ts.taskscheduler.application.data.enums.NotificationResult; + +public record NotificationResultDetails( + String taskId, + NotificationResult notificationResult, + String errorMessage +) { +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/data/enums/NotificationResult.java b/src/main/java/com/javanauta/ts/taskscheduler/application/data/enums/NotificationResult.java new file mode 100644 index 0000000..f13814d --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/data/enums/NotificationResult.java @@ -0,0 +1,7 @@ +package com.javanauta.ts.taskscheduler.application.data.enums; + +public enum NotificationResult { + SUCCESS, + PERMANENT_FAILURE, + TEMPORARY_FAILURE +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index df59d75..fad79f1 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -1,25 +1,22 @@ package com.javanauta.ts.taskscheduler.application.service; -import com.javanauta.ts.events.notification.NotificationCompletedEvent; -import com.javanauta.ts.events.notification.NotificationFailedEvent; -import com.javanauta.ts.events.notification.NotificationRequestEvent; +import com.javanauta.ts.taskscheduler.application.data.NotificationResultDetails; +import com.javanauta.ts.taskscheduler.application.data.enums.NotificationResult; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; -import com.javanauta.ts.taskscheduler.ports.out.persistence.TaskPersister; -import com.javanauta.ts.taskscheduler.ports.out.security.PrincipalProvider; -import com.javanauta.ts.taskscheduler.ports.out.messaging.NotificationRequestPublisher; import com.javanauta.ts.taskscheduler.domain.data.TaskData; import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; +import com.javanauta.ts.taskscheduler.ports.out.messaging.NotificationRequestPublisher; +import com.javanauta.ts.taskscheduler.ports.out.persistence.TaskPersister; +import com.javanauta.ts.taskscheduler.ports.out.security.PrincipalProvider; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import javax.management.Notification; import java.time.Instant; import java.util.List; -import java.util.UUID; @Service @RequiredArgsConstructor @@ -75,15 +72,7 @@ public void requestTaskNotification(Task task) { return; } - NotificationRequestEvent event = NotificationRequestEvent.create( - task.getId(), - task.getName(), - task.getDescription(), - task.getScheduledDateTime(), - task.getUserEmail(), - task.getTimeZoneId().toString()); - - notificationRequestPublisher.publishNotificationRequest(event); + notificationRequestPublisher.publishNotificationRequest(task); updateTaskStatus(task, NotificationStatus.DISPATCHED); log.info("Notification request for Task '{}' was successfully dispatched", task.getId()); @@ -91,20 +80,25 @@ public void requestTaskNotification(Task task) { // Messaging Broker - public void processTaskNotificationCompletion(NotificationCompletedEvent event) { - Task task = getTaskOrThrow(event.taskId()); + public void processTaskNotificationCompletion(NotificationResultDetails resultDetails) { + String taskId = resultDetails.taskId(); + Task task = getTaskOrThrow(taskId); updateTaskStatus(task, NotificationStatus.NOTIFIED); - log.info("Notification for Task '{}' was successfully completed", event.taskId()); + log.info("Notification for Task '{}' was successfully completed", taskId); } - public void processTaskNotificationFailure(NotificationFailedEvent event) { - log.info("Notification for Task '{}' failed ({})", event.taskId(), event.failureType().name().toLowerCase()); - Task task = getTaskOrThrow(event.taskId()); + public void processTaskNotificationFailure(NotificationResultDetails resultDetails) { + NotificationResult notificationResult = resultDetails.notificationResult(); + String taskId = resultDetails.taskId(); + + log.info("Notification for Task '{}' failed ({})", taskId, notificationResult.name().toLowerCase()); + Task task = getTaskOrThrow(taskId); - NotificationStatus newStatus = switch (event.failureType()) { - case TEMPORARY -> NotificationStatus.PENDING_RETRY; - case PERMANENT -> NotificationStatus.FAILED; + NotificationStatus newStatus = switch (notificationResult) { + case TEMPORARY_FAILURE -> NotificationStatus.PENDING_RETRY; + case PERMANENT_FAILURE -> NotificationStatus.FAILED; + default -> throw new IllegalArgumentException("Only 'FAILURE' results are expected here"); }; updateTaskStatus(task, newStatus); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/messaging/NotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/messaging/NotificationRequestPublisher.java index ad6242b..1d4b71a 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/messaging/NotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/messaging/NotificationRequestPublisher.java @@ -1,7 +1,7 @@ package com.javanauta.ts.taskscheduler.ports.out.messaging; -import com.javanauta.ts.events.notification.NotificationRequestEvent; +import com.javanauta.ts.taskscheduler.domain.model.Task; public interface NotificationRequestPublisher { - void publishNotificationRequest(NotificationRequestEvent event); + void publishNotificationRequest(Task task); } From 6eadcf8adc7f6deb3e78627e61d83c47ef2a3dfb Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 10 Jul 2026 11:02:39 +0100 Subject: [PATCH 13/51] WIP: introduce input validation to RabbitMQ listener and a Recoverer for logging of both Completed and Failed events --- .../messaging/NotificationEventRecoverer.java | 92 +++++++++++++++++++ .../RabbitNotificationCompletedListener.java | 13 ++- .../RabbitNotificationFailedListener.java | 9 +- .../NotificationEventValidator.java | 24 +++++ .../exception/ApplicationException.java | 1 - 5 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/validation/NotificationEventValidator.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java new file mode 100644 index 0000000..b4b89b7 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java @@ -0,0 +1,92 @@ +package com.javanauta.ts.taskscheduler.adapters.in.messaging; + +import com.javanauta.ts.events.notification.Event; +import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; +import jakarta.validation.ConstraintViolationException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.retry.MessageRecoverer; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Component +@AllArgsConstructor +@Slf4j +public class NotificationEventRecoverer implements MessageRecoverer { + private final MessageConverter messageConverter; + + @Override + public void recover(Message message, Throwable cause) { + // Pre-listener conversion failure + if (findCause(cause, MessageConversionException.class) != null) { + log.error(""" + Invalid Event received. + Payload: + {} + """, new String(message.getBody(), StandardCharsets.UTF_8), cause); + return; + } + + Event event; + try { + event = (Event) messageConverter.fromMessage(message); + } catch (MessageConversionException e) { + log.error(""" + Attempt to deserialize Event within the Recoverer failed. + Payload: + {} + """, new String(message.getBody(), StandardCharsets.UTF_8), e); + return; + } + + // Validation errors + ConstraintViolationException constraintViolationException = findCause(cause, ConstraintViolationException.class); + if (constraintViolationException != null) { + List constraintMessages = constraintViolationException + .getConstraintViolations() + .stream() + .map(violation -> violation.getPropertyPath() + " " + violation.getMessage()) + .toList(); + + log.error("Validation of {} for Task {} has failed: {}", + event.getClass().getSimpleName(), + "", + String.join(", ", constraintMessages)); + + return; + } + + // Business error + String errorMsg; + ApplicationException appException = findCause(cause, ApplicationException.class); + if (appException != null) { + log.error("Processing of {} for Task {} has failed due to a business error ({}): {}", + event.getClass().getSimpleName(), + "", + appException.getCode().getIdentifier(), + appException.getMessage()); + return; + } + + // Any other unhandled exception + log.error("Processing of {} for Task {} has failed due to an internal error", + event.getClass().getSimpleName(), + "", + cause); + } + + private T findCause(Throwable throwable, Class type) { + while (throwable != null) { + if (type.isInstance(throwable)) { + return type.cast(throwable); + } + throwable = throwable.getCause(); + } + return null; + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java index 673849b..5c9b25a 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java @@ -2,10 +2,12 @@ import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationCompletedEvent; +import com.javanauta.ts.taskscheduler.adapters.in.messaging.validation.NotificationEventValidator; import com.javanauta.ts.taskscheduler.application.data.NotificationResultDetails; import com.javanauta.ts.taskscheduler.application.data.enums.NotificationResult; import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; +import jakarta.validation.ConstraintViolationException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.AmqpRejectAndDontRequeueException; @@ -16,11 +18,17 @@ @AllArgsConstructor @Slf4j public class RabbitNotificationCompletedListener { - private final TaskService taskService; + private final NotificationEventValidator notificationEventValidator; @RabbitListener(queues = Queues.NOTIFICATION_COMPLETED) public void handleNotificationCompleted(NotificationCompletedEvent event) { + try { + notificationEventValidator.validate(event); + } catch (ConstraintViolationException ex) { + throw new AmqpRejectAndDontRequeueException(ex); + } + NotificationResultDetails resultDetails = new NotificationResultDetails( event.taskId(), NotificationResult.SUCCESS, @@ -28,8 +36,7 @@ public void handleNotificationCompleted(NotificationCompletedEvent event) { try { taskService.processTaskNotificationCompletion(resultDetails); - } catch ( - ApplicationException ex) { + } catch (ApplicationException ex) { // No retryable exceptions. If any exception is thrown, is probably database related and could be recovered throw new AmqpRejectAndDontRequeueException(ex); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java index 810d0cf..4d47ebb 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java @@ -2,10 +2,12 @@ import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationFailedEvent; +import com.javanauta.ts.taskscheduler.adapters.in.messaging.validation.NotificationEventValidator; import com.javanauta.ts.taskscheduler.application.data.NotificationResultDetails; import com.javanauta.ts.taskscheduler.application.data.enums.NotificationResult; import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; +import jakarta.validation.ConstraintViolationException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.AmqpRejectAndDontRequeueException; @@ -16,11 +18,16 @@ @RequiredArgsConstructor @Slf4j public class RabbitNotificationFailedListener { - private final TaskService taskService; + private final NotificationEventValidator notificationEventValidator; @RabbitListener(queues = Queues.NOTIFICATION_FAILED) public void handleNotificationFailed(NotificationFailedEvent event) { + try { + notificationEventValidator.validate(event); + } catch (ConstraintViolationException ex) { + throw new AmqpRejectAndDontRequeueException(ex); + } NotificationResult result = switch (event.failureType()) { case PERMANENT -> NotificationResult.PERMANENT_FAILURE; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/validation/NotificationEventValidator.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/validation/NotificationEventValidator.java new file mode 100644 index 0000000..dbefe69 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/validation/NotificationEventValidator.java @@ -0,0 +1,24 @@ +package com.javanauta.ts.taskscheduler.adapters.in.messaging.validation; + +import com.javanauta.ts.events.notification.Event; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Set; + +@Component +@RequiredArgsConstructor +public class NotificationEventValidator { + private final Validator validator; + + public void validate(T event) { + Set> violations = validator.validate(event); + + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/shared/exception/ApplicationException.java b/src/main/java/com/javanauta/ts/taskscheduler/shared/exception/ApplicationException.java index 3bca8b1..effb6fb 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/shared/exception/ApplicationException.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/shared/exception/ApplicationException.java @@ -6,7 +6,6 @@ @Getter public class ApplicationException extends RuntimeException { - private final ExceptionCode code; private final String message; private final List validationExceptionDetails; From 51224c496ee468982088dbfb6dd9ad9ae64af3c0 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 10 Jul 2026 11:32:16 +0100 Subject: [PATCH 14/51] Adjustments regarding reorganization of packages/names and introduction of NotificationEvent interface in the events library --- .../in/messaging/NotificationEventRecoverer.java | 12 ++++++------ .../RabbitNotificationCompletedListener.java | 2 +- .../messaging/RabbitNotificationFailedListener.java | 2 +- .../adapters/in/messaging/config/RabbitInConfig.java | 6 +++--- .../validation/NotificationEventValidator.java | 2 +- .../RabbitNotificationRequestPublisher.java | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java index b4b89b7..5c28c37 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java @@ -1,6 +1,6 @@ package com.javanauta.ts.taskscheduler.adapters.in.messaging; -import com.javanauta.ts.events.notification.Event; +import com.javanauta.ts.events.notification.NotificationEvent; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import jakarta.validation.ConstraintViolationException; import lombok.AllArgsConstructor; @@ -32,9 +32,9 @@ public void recover(Message message, Throwable cause) { return; } - Event event; + NotificationEvent event; try { - event = (Event) messageConverter.fromMessage(message); + event = (NotificationEvent) messageConverter.fromMessage(message); } catch (MessageConversionException e) { log.error(""" Attempt to deserialize Event within the Recoverer failed. @@ -55,7 +55,7 @@ public void recover(Message message, Throwable cause) { log.error("Validation of {} for Task {} has failed: {}", event.getClass().getSimpleName(), - "", + event.taskId(), String.join(", ", constraintMessages)); return; @@ -67,7 +67,7 @@ public void recover(Message message, Throwable cause) { if (appException != null) { log.error("Processing of {} for Task {} has failed due to a business error ({}): {}", event.getClass().getSimpleName(), - "", + event.taskId(), appException.getCode().getIdentifier(), appException.getMessage()); return; @@ -76,7 +76,7 @@ public void recover(Message message, Throwable cause) { // Any other unhandled exception log.error("Processing of {} for Task {} has failed due to an internal error", event.getClass().getSimpleName(), - "", + event.taskId(), cause); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java index 5c9b25a..ef82262 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationCompletedListener.java @@ -1,7 +1,7 @@ package com.javanauta.ts.taskscheduler.adapters.in.messaging; -import com.javanauta.ts.events.messaging.Queues; import com.javanauta.ts.events.notification.NotificationCompletedEvent; +import com.javanauta.ts.events.notification.messaging.Queues; import com.javanauta.ts.taskscheduler.adapters.in.messaging.validation.NotificationEventValidator; import com.javanauta.ts.taskscheduler.application.data.NotificationResultDetails; import com.javanauta.ts.taskscheduler.application.data.enums.NotificationResult; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java index 4d47ebb..9d5ada7 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/RabbitNotificationFailedListener.java @@ -1,6 +1,6 @@ package com.javanauta.ts.taskscheduler.adapters.in.messaging; -import com.javanauta.ts.events.messaging.Queues; +import com.javanauta.ts.events.notification.messaging.Queues; import com.javanauta.ts.events.notification.NotificationFailedEvent; import com.javanauta.ts.taskscheduler.adapters.in.messaging.validation.NotificationEventValidator; import com.javanauta.ts.taskscheduler.application.data.NotificationResultDetails; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java index 8643dd0..95f7dc0 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java @@ -1,8 +1,8 @@ package com.javanauta.ts.taskscheduler.adapters.in.messaging.config; -import com.javanauta.ts.events.messaging.Exchanges; -import com.javanauta.ts.events.messaging.Queues; -import com.javanauta.ts.events.messaging.RoutingKeys; +import com.javanauta.ts.events.notification.messaging.Exchanges; +import com.javanauta.ts.events.notification.messaging.Queues; +import com.javanauta.ts.events.notification.messaging.RoutingKeys; import org.aopalliance.aop.Advice; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.core.Binding; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/validation/NotificationEventValidator.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/validation/NotificationEventValidator.java index dbefe69..8ea1579 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/validation/NotificationEventValidator.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/validation/NotificationEventValidator.java @@ -1,6 +1,6 @@ package com.javanauta.ts.taskscheduler.adapters.in.messaging.validation; -import com.javanauta.ts.events.notification.Event; +import com.javanauta.ts.events.Event; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validator; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java index 70c7f6b..c217891 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/messaging/RabbitNotificationRequestPublisher.java @@ -1,8 +1,8 @@ package com.javanauta.ts.taskscheduler.adapters.out.messaging; -import com.javanauta.ts.events.messaging.Exchanges; -import com.javanauta.ts.events.messaging.RoutingKeys; import com.javanauta.ts.events.notification.NotificationRequestEvent; +import com.javanauta.ts.events.notification.messaging.Exchanges; +import com.javanauta.ts.events.notification.messaging.RoutingKeys; import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.ports.out.messaging.NotificationRequestPublisher; import lombok.AllArgsConstructor; From d6e1f1be89c5ab11b9d6db2900646c3131c51553 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 10 Jul 2026 12:43:44 +0100 Subject: [PATCH 15/51] Update AMQP dependency version to fix NullPointerException for header's 'priority' and final details to get the Recoverer working --- build.gradle | 4 ++-- .../in/messaging/NotificationEventRecoverer.java | 2 +- .../adapters/in/messaging/config/RabbitInConfig.java | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 119098c..499c826 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0' + id 'org.springframework.boot' version '4.0.1' id 'io.spring.dependency-management' version '1.1.7' id "org.sonarqube" version "7.2.3.7755" } @@ -34,7 +34,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webmvc' - implementation 'org.springframework.boot:spring-boot-starter-actuator:4.0.0' + implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'io.jsonwebtoken:jjwt-api:0.13.0' diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java index 5c28c37..43d2b36 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/NotificationEventRecoverer.java @@ -65,7 +65,7 @@ public void recover(Message message, Throwable cause) { String errorMsg; ApplicationException appException = findCause(cause, ApplicationException.class); if (appException != null) { - log.error("Processing of {} for Task {} has failed due to a business error ({}): {}", + log.error("Processing of {} for Task {} has failed due to a business error ({}): {}", event.getClass().getSimpleName(), event.taskId(), appException.getCode().getIdentifier(), diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java index 95f7dc0..b3b8a9b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java @@ -3,6 +3,7 @@ import com.javanauta.ts.events.notification.messaging.Exchanges; import com.javanauta.ts.events.notification.messaging.Queues; import com.javanauta.ts.events.notification.messaging.RoutingKeys; +import com.javanauta.ts.taskscheduler.adapters.in.messaging.NotificationEventRecoverer; import org.aopalliance.aop.Advice; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.core.Binding; @@ -12,6 +13,7 @@ import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -71,12 +73,15 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( } @Bean - public Advice retryAdvice() { + public Advice retryAdvice(NotificationEventRecoverer notificationEventRecoverer) { return RetryInterceptorBuilder.stateless() .configureRetryPolicy(builder -> builder .maxRetries(3) - .excludes(AmqpRejectAndDontRequeueException.class)) + .excludes( + MessageConversionException.class, + AmqpRejectAndDontRequeueException.class)) .backOffOptions(60000, 1.0, 60000) + .recoverer(notificationEventRecoverer) .build(); } } From b25978474cdb90d4fd1809a1cdc0db3968cb40c8 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Mon, 13 Jul 2026 20:09:33 +0100 Subject: [PATCH 16/51] Remove RabbitMQ resources creation (moved to infra repo) and add env variables for deployed environment --- .../in/messaging/config/RabbitInConfig.java | 36 ------------------- src/main/resources/application.properties | 10 +++--- 2 files changed, 6 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java index b3b8a9b..cc8423f 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java @@ -20,42 +20,6 @@ @Configuration public class RabbitInConfig { - - @Bean - public TopicExchange notificationExchange() { - return new TopicExchange(Exchanges.NOTIFICATION); - } - - @Bean - public Queue notificationCompletedQueue() { - return new Queue(Queues.NOTIFICATION_COMPLETED); - } - - @Bean - public Binding notificationCompletedBinding( - Queue notificationCompletedQueue, - TopicExchange notificationExchange) { - - return BindingBuilder - .bind(notificationCompletedQueue) - .to(notificationExchange) - .with(RoutingKeys.NOTIFICATION_COMPLETED); - } - - @Bean - public Queue notificationFailedQueue() { return new Queue(Queues.NOTIFICATION_FAILED); } - - @Bean - public Binding notificationFailedBinding( - Queue notificationFailedQueue, - TopicExchange notificationExchange) { - - return BindingBuilder - .bind(notificationFailedQueue) - .to(notificationExchange) - .with(RoutingKeys.NOTIFICATION_FAILED); - } - @Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( ConnectionFactory connectionFactory, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d4a929d..0e96e59 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -19,7 +19,9 @@ ts.user.service.uri=${TS_USER_SERVICE_URI:http://localhost:8080/user} ts.jwt.secret=${TS_JWT_SECRET} # AMQP configuration -spring.rabbitmq.host=localhost -spring.rabbitmq.port=5672 -spring.rabbitmq.username=guest -spring.rabbitmq.password=guest + +spring.rabbitmq.host=${SPRING_RABBITMQ_HOST:localhost} +spring.rabbitmq.port=${SPRING_RABBITMQ_PORT:5672} +spring.rabbitmq.username=${SPRING_RABBITMQ_USERNAME:guest} +spring.rabbitmq.password=${SPRING_RABBITMQ_PASSWORD:guest} +spring.rabbitmq.virtual-host=${SPRING_RABBITMQ_VIRTUAL_HOST:/} From 117b4a7ea6ea6dd04b5eaa61ee36e5621fff053f Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Tue, 14 Jul 2026 14:28:09 +0100 Subject: [PATCH 17/51] Adjustments to use ts:events library published on JitPack --- build.gradle | 19 +++++++------------ settings.gradle | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 499c826..cce1dbd 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '4.0.1' id 'io.spring.dependency-management' version '1.1.7' - id "org.sonarqube" version "7.2.3.7755" + id 'org.sonarqube' version '7.2.3.7755' } group = 'com.javanauta.ts' @@ -21,13 +21,8 @@ configurations { } } -repositories { - mavenCentral() - mavenLocal() -} - ext { - set('springCloudVersion', "2025.1.0") + set('springCloudVersion', '2025.1.0') } dependencies { @@ -41,8 +36,8 @@ dependencies { implementation 'org.mapstruct:mapstruct:1.6.3' implementation 'org.mapstruct:mapstruct-processor:1.6.3' implementation 'com.fasterxml.jackson.core:jackson-databind' - implementation "org.springframework.boot:spring-boot-starter-amqp" - implementation "com.javanauta.ts:events:1.0.0" + implementation 'org.springframework.boot:spring-boot-starter-amqp' + implementation 'com.github.enzobbom:ts-events:v1.0.0' compileOnly 'org.projectlombok:lombok' @@ -71,8 +66,8 @@ tasks.named('test') { sonarqube { properties { - property "sonar.projectKey", "task-scheduler" - property "sonar.host.url", "http://localhost:9000" - property "sonar.login", "squ_dc76e8eeb6ed2d1dfc066db1af123d8dde47f3f6" + property 'sonar.projectKey', 'task-scheduler' + property 'sonar.host.url', 'http://localhost:9000' + property 'sonar.login', 'squ_dc76e8eeb6ed2d1dfc066db1af123d8dde47f3f6' } } diff --git a/settings.gradle b/settings.gradle index 67d2973..1e7f786 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,17 @@ rootProject.name = 'task-scheduler' + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + + repositories { + mavenCentral() + + maven { + url = uri("https://jitpack.io") + + content { + includeGroup("com.github.enzobbom") + } + } + } +} From bc43589937095b61dbb89f99b0e8be4d87fec1ad Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Tue, 14 Jul 2026 16:41:05 +0100 Subject: [PATCH 18/51] Small clean up --- .../adapters/in/messaging/config/RabbitInConfig.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java index cc8423f..b594383 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/messaging/config/RabbitInConfig.java @@ -1,15 +1,8 @@ package com.javanauta.ts.taskscheduler.adapters.in.messaging.config; -import com.javanauta.ts.events.notification.messaging.Exchanges; -import com.javanauta.ts.events.notification.messaging.Queues; -import com.javanauta.ts.events.notification.messaging.RoutingKeys; import com.javanauta.ts.taskscheduler.adapters.in.messaging.NotificationEventRecoverer; import org.aopalliance.aop.Advice; import org.springframework.amqp.AmqpRejectAndDontRequeueException; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; From 2e77434bbfe2645e79ef7cfa790bd45577f68a1d Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Wed, 15 Jul 2026 10:49:54 +0100 Subject: [PATCH 19/51] Add version to the API --- .../adapters/in/web/controller/TaskController.java | 3 ++- .../ts/taskscheduler/adapters/in/web/path/ApiPaths.java | 7 +++++++ .../taskscheduler/adapters/in/web/path/ApiVersions.java | 8 ++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiPaths.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiVersions.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java index e4e1b6f..6998904 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java @@ -1,5 +1,6 @@ package com.javanauta.ts.taskscheduler.adapters.in.web.controller; +import com.javanauta.ts.taskscheduler.adapters.in.web.path.ApiPaths; import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.CreateTaskRequestDTO; @@ -17,7 +18,7 @@ import java.util.List; @RestController -@RequestMapping("/task") +@RequestMapping(ApiPaths.TASKS_V1) @RequiredArgsConstructor @Validated public class TaskController { diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiPaths.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiPaths.java new file mode 100644 index 0000000..c99de72 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiPaths.java @@ -0,0 +1,7 @@ +package com.javanauta.ts.taskscheduler.adapters.in.web.path; + +public final class ApiPaths { + private ApiPaths() {} + + public static final String TASKS_V1 = ApiVersions.V1 + "/tasks"; +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiVersions.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiVersions.java new file mode 100644 index 0000000..0e5dda7 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiVersions.java @@ -0,0 +1,8 @@ +package com.javanauta.ts.taskscheduler.adapters.in.web.path; + +public final class ApiVersions { + private ApiVersions() {} + + public static final String API = "/api"; + public static final String V1 = API + "/v1"; +} From bdd8eb46de666a04ed922dbe5c854288ddd9cf6a Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 16 Jul 2026 11:14:11 +0100 Subject: [PATCH 20/51] Replace usage API Response objects by new shared api-contract library --- build.gradle | 1 + .../in/web/controller/TaskController.java | 8 +++---- .../web/exception/GlobalExceptionHandler.java | 18 +++++++------- .../adapters/in/web/path/ApiPaths.java | 2 ++ .../adapters/in/web/path/ApiVersions.java | 8 ------- .../in/web/response/ErrorResponse.java | 24 ------------------- .../adapters/in/web/response/Response.java | 16 ------------- .../in/web/response/SuccessResponse.java | 23 ------------------ .../web/response/ValidationErrorDetail.java | 11 --------- .../in/web/response/enums/ResponseStatus.java | 6 ----- 10 files changed, 16 insertions(+), 101 deletions(-) delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiVersions.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ErrorResponse.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/Response.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/SuccessResponse.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ValidationErrorDetail.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/enums/ResponseStatus.java diff --git a/build.gradle b/build.gradle index cce1dbd..4a7104d 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.springframework.boot:spring-boot-starter-amqp' implementation 'com.github.enzobbom:ts-events:v1.0.0' + implementation 'com.github.enzobbom:ts-api-contract:v1.0.0' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java index 6998904..0d7658a 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java @@ -6,7 +6,7 @@ import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.CreateTaskRequestDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.UpdateTaskRequestDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.mapper.TaskMapper; -import com.javanauta.ts.taskscheduler.adapters.in.web.response.SuccessResponse; +import com.javanauta.ts.apicontract.response.SuccessResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; @@ -34,7 +34,7 @@ public ResponseEntity createTask( HttpStatus httpCode = HttpStatus.OK; SuccessResponse successResponse = new SuccessResponse( - httpCode, + httpCode.value(), taskMapper.toTaskDTO(createdTask)); return ResponseEntity.status(httpCode).body(successResponse); @@ -46,7 +46,7 @@ public ResponseEntity findTasksByUserEmail() { HttpStatus httpCode = HttpStatus.OK; SuccessResponse successResponse = new SuccessResponse( - httpCode, + httpCode.value(), taskMapper.toTaskDTOList(tasks)); return ResponseEntity.status(httpCode).body(successResponse); @@ -69,7 +69,7 @@ public ResponseEntity updateTask( HttpStatus httpCode = HttpStatus.OK; SuccessResponse successResponse = new SuccessResponse( - httpCode, + httpCode.value(), taskMapper.toTaskDTO(task)); return ResponseEntity.status(httpCode).body(successResponse); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java index ad961c8..42ef100 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java @@ -1,10 +1,10 @@ package com.javanauta.ts.taskscheduler.adapters.in.web.exception; +import com.javanauta.ts.apicontract.response.ValidationErrorDetail; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; import com.javanauta.ts.taskscheduler.domain.exception.enums.DomainExceptionCode; import com.javanauta.ts.taskscheduler.adapters.in.web.exception.enums.PresentationExceptionCode; -import com.javanauta.ts.taskscheduler.adapters.in.web.response.ErrorResponse; -import com.javanauta.ts.taskscheduler.adapters.in.web.response.ValidationErrorDetail; +import com.javanauta.ts.apicontract.response.ErrorResponse; import com.javanauta.ts.taskscheduler.shared.exception.ApplicationException; import com.javanauta.ts.taskscheduler.shared.exception.ExceptionCode; import com.javanauta.ts.taskscheduler.shared.exception.enums.ValidationExceptionSourceType; @@ -84,7 +84,7 @@ public ResponseEntity handleMethodArgumentNotValidException( allValidationErrors.addAll(fieldErrors); ErrorResponse errorResponse = new ErrorResponse( - httpCode, + httpCode.value(), errorIdentifier, exceptionCode.getDefaultMessage(), allValidationErrors); @@ -108,7 +108,7 @@ public ResponseEntity handleConstraintViolationException(Constrai .toList(); ErrorResponse errorResponse = new ErrorResponse( - httpCode, + httpCode.value(), errorIdentifier, exceptionCode.getDefaultMessage(), constraintViolations); @@ -127,7 +127,7 @@ public ResponseEntity handleTypeMismatchException(MethodArgumentT String message = "Invalid value for parameter: " + ex.getName(); ErrorResponse errorResponse = new ErrorResponse( - httpCode, + httpCode.value(), errorIdentifier, message, List.of()); @@ -143,7 +143,7 @@ public ResponseEntity handleHttpMessageNotReadableException(HttpM String errorIdentifier = exceptionCode.getIdentifier(); ErrorResponse errorResponse = new ErrorResponse( - httpCode, + httpCode.value(), errorIdentifier, exceptionCode.getDefaultMessage(), List.of()); @@ -160,7 +160,7 @@ public ResponseEntity handleMissingServletRequestParameterExcepti String message = "Missing required parameter: " + ex.getParameterName(); ErrorResponse errorResponse = new ErrorResponse( - httpCode, + httpCode.value(), errorIdentifier, message, List.of()); @@ -184,7 +184,7 @@ public ResponseEntity handleApplicationException(ApplicationExcep .toList(); ErrorResponse errorResponse = new ErrorResponse( - httpCode, + httpCode.value(), exceptionCode.getIdentifier(), ex.getMessage(), validationErrors); @@ -203,7 +203,7 @@ public ResponseEntity handleGenericException(Exception ex) { String errorIdentifier = exceptionCode.getIdentifier(); ErrorResponse errorResponse = new ErrorResponse( - httpCode, + httpCode.value(), errorIdentifier, exceptionCode.getDefaultMessage(), List.of()); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiPaths.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiPaths.java index c99de72..5469ab6 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiPaths.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiPaths.java @@ -1,5 +1,7 @@ package com.javanauta.ts.taskscheduler.adapters.in.web.path; +import com.javanauta.ts.apicontract.version.ApiVersions; + public final class ApiPaths { private ApiPaths() {} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiVersions.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiVersions.java deleted file mode 100644 index 0e5dda7..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/path/ApiVersions.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.web.path; - -public final class ApiVersions { - private ApiVersions() {} - - public static final String API = "/api"; - public static final String V1 = API + "/v1"; -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ErrorResponse.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ErrorResponse.java deleted file mode 100644 index 659edfe..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ErrorResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.web.response; - -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.javanauta.ts.taskscheduler.adapters.in.web.response.enums.ResponseStatus; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -import java.util.List; - -@Getter -@JsonPropertyOrder({"status", "code", "errorCode", "message", "validationErrors"}) -public class ErrorResponse extends Response { - - private final String errorCode; - private final String message; - private final List validationErrors; - - public ErrorResponse(HttpStatus code, String errorCode, String message, List validationErrorDetails) { - super(ResponseStatus.ERROR, code); - this.errorCode = errorCode; - this.message = message; - this.validationErrors = validationErrorDetails; - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/Response.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/Response.java deleted file mode 100644 index 6ed8a7e..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/Response.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.web.response; - -import com.javanauta.ts.taskscheduler.adapters.in.web.response.enums.ResponseStatus; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -public class Response { - protected final ResponseStatus status; - protected final int code; - - public Response(ResponseStatus status, HttpStatus code) { - this.status = status; - this.code = code.value(); - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/SuccessResponse.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/SuccessResponse.java deleted file mode 100644 index 8501e35..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/SuccessResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.web.response; - -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.javanauta.ts.taskscheduler.adapters.in.web.response.enums.ResponseStatus; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@JsonPropertyOrder({"status", "code", "data"}) -public class SuccessResponse extends Response { - - private final T data; - - public SuccessResponse(HttpStatus code, T data) { - super(ResponseStatus.SUCCESS, code); - this.data = data; - } - - public SuccessResponse(HttpStatus code) { - super(ResponseStatus.SUCCESS, code); - this.data = null; - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ValidationErrorDetail.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ValidationErrorDetail.java deleted file mode 100644 index 5d0c105..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/ValidationErrorDetail.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.web.response; - -import com.fasterxml.jackson.annotation.JsonPropertyOrder; - -@JsonPropertyOrder({"sourceType", "source", "message"}) -public record ValidationErrorDetail( - String sourceType, - String source, - String message -) { -} \ No newline at end of file diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/enums/ResponseStatus.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/enums/ResponseStatus.java deleted file mode 100644 index 686d1b9..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/response/enums/ResponseStatus.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.web.response.enums; - -public enum ResponseStatus { - SUCCESS, - ERROR -} From 5de914284a56966799d2931637da1060e23e63ab Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 16 Jul 2026 11:58:27 +0100 Subject: [PATCH 21/51] Add indexes to Task fields for better read performance --- .../com/javanauta/ts/taskscheduler/domain/model/Task.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java index fb5c40b..68290bc 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java @@ -8,6 +8,8 @@ import com.javanauta.ts.taskscheduler.shared.exception.ValidationExceptionDetail; import lombok.*; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; import java.time.Instant; @@ -20,14 +22,18 @@ @AllArgsConstructor @Builder @Document("task") +@CompoundIndex( + name = "notification_schedule_idx", + def = "{'notificationStatus': 1, 'scheduledDateTime': 1}" +) public class Task { - @Id private String id; private String name; private String description; private Instant creationDateTime; private Instant scheduledDateTime; + @Indexed private String userEmail; private Instant modificationDateTime; private NotificationStatus notificationStatus; From 75692f7fb13abd5ac56291cf8a16dbae47de3a96 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 16 Jul 2026 12:49:22 +0100 Subject: [PATCH 22/51] Make method that retrieves Tasks by period also filter by NotificationStatus + service/domain adjustments for better ownership of domain invariants --- .../in/scheduler/TaskNotificationTrigger.java | 2 +- .../out/persistence/MongoTaskRepository.java | 4 +++- .../out/persistence/TaskPersisterAdapter.java | 6 ++++-- .../application/service/TaskService.java | 12 ++++++------ .../ts/taskscheduler/domain/model/Task.java | 3 +-- .../domain/model/enums/NotificationStatus.java | 8 +++++++- .../ports/out/persistence/TaskPersister.java | 4 +++- 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/scheduler/TaskNotificationTrigger.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/scheduler/TaskNotificationTrigger.java index 3888c7e..88aebb1 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/scheduler/TaskNotificationTrigger.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/scheduler/TaskNotificationTrigger.java @@ -22,7 +22,7 @@ public void findTasksInNextNotificationPeriod() { long notificationPeriod = 1L; // in hours - currently hard coded but meant to be task setting Instant nowTime = Instant.now(); - List tasksList = taskService.findTasksByTimePeriod(nowTime, nowTime.plus(notificationPeriod, ChronoUnit.HOURS)); + List tasksList = taskService.findTasksToNotify(nowTime, nowTime.plus(notificationPeriod, ChronoUnit.HOURS)); tasksList.forEach(taskService::requestTaskNotification); } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java index 568760a..d6a46f1 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java @@ -1,14 +1,16 @@ package com.javanauta.ts.taskscheduler.adapters.out.persistence; import com.javanauta.ts.taskscheduler.domain.model.Task; +import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.stereotype.Repository; import java.time.Instant; +import java.util.Collection; import java.util.List; @Repository public interface MongoTaskRepository extends MongoRepository { - List findByScheduledDateTimeBetween(Instant initialDateTime, Instant finalDateTime); + List findByNotificationStatusInAndScheduledDateTimeBetween(Collection statuses, Instant initialDateTime, Instant finalDateTime); List findByUserEmail(String userEmail); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java index 0bd884b..21048af 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java @@ -1,11 +1,13 @@ package com.javanauta.ts.taskscheduler.adapters.out.persistence; import com.javanauta.ts.taskscheduler.domain.model.Task; +import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; import com.javanauta.ts.taskscheduler.ports.out.persistence.TaskPersister; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import java.time.Instant; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -30,8 +32,8 @@ public Optional findById(String id) { } @Override - public List findByScheduledDateTimeBetween(Instant initialDateTime, Instant finalDateTime) { - return taskRepository.findByScheduledDateTimeBetween(initialDateTime, finalDateTime); + public List findByNotificationStatusInAndScheduledDateTimeBetween(Collection statuses, Instant initialDateTime, Instant finalDateTime) { + return taskRepository.findByNotificationStatusInAndScheduledDateTimeBetween(statuses, initialDateTime, finalDateTime); } @Override diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index fad79f1..592c988 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -22,7 +22,6 @@ @RequiredArgsConstructor @Slf4j public class TaskService { - private final TaskPersister taskPersister; private final PrincipalProvider principalProvider; private final NotificationRequestPublisher notificationRequestPublisher; @@ -63,14 +62,15 @@ public Task updateTask(TaskData taskData, String id) { // Scheduler - public List findTasksByTimePeriod(Instant initialDateTime, Instant finalDateTime) { - return taskPersister.findByScheduledDateTimeBetween(initialDateTime, finalDateTime); + public List findTasksToNotify(Instant initialDateTime, Instant finalDateTime) { + return taskPersister.findByNotificationStatusInAndScheduledDateTimeBetween( + NotificationStatus.notifiableStatuses(), + initialDateTime, + finalDateTime); } public void requestTaskNotification(Task task) { - if (!task.canBeNotified()) { - return; - } + if (!task.canBeNotified()) { return; } notificationRequestPublisher.publishNotificationRequest(task); updateTaskStatus(task, NotificationStatus.DISPATCHED); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java index 68290bc..6ff3550 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java @@ -81,8 +81,7 @@ public void update(TaskData taskData) { } public boolean canBeNotified() { - return notificationStatus == NotificationStatus.PENDING - || notificationStatus == NotificationStatus.PENDING_RETRY; + return NotificationStatus.notifiableStatuses().contains(notificationStatus); } private void validateScheduledDate() { diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java index 29231ea..880ca1f 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/enums/NotificationStatus.java @@ -1,9 +1,15 @@ package com.javanauta.ts.taskscheduler.domain.model.enums; +import java.util.Set; + public enum NotificationStatus { PENDING, DISPATCHED, NOTIFIED, PENDING_RETRY, - FAILED + FAILED; + + public static Set notifiableStatuses() { + return Set.of(PENDING, PENDING_RETRY); + } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java index 40d221c..406dabf 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java @@ -1,8 +1,10 @@ package com.javanauta.ts.taskscheduler.ports.out.persistence; import com.javanauta.ts.taskscheduler.domain.model.Task; +import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; import java.time.Instant; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -10,6 +12,6 @@ public interface TaskPersister { Task save(Task task); void deleteById(String id); Optional findById(String id); - List findByScheduledDateTimeBetween(Instant initialDateTime, Instant finalDateTime); + List findByNotificationStatusInAndScheduledDateTimeBetween(Collection statuses, Instant initialDateTime, Instant finalDateTime); List findByUserEmail(String userEmail); } From 3e1d3b1c59c4b794f2a3f1d85720c54b79c3dd20 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 16 Jul 2026 12:53:13 +0100 Subject: [PATCH 23/51] Remove @Transactional from update method as it deals with a single collection/entity only --- .../ts/taskscheduler/application/service/TaskService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 592c988..8c1b948 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -13,7 +13,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.List; @@ -50,7 +49,6 @@ public void deleteTask(String id) { log.info("Task {} deleted", id); } - @Transactional public Task updateTask(TaskData taskData, String id) { Task task = getTaskOrThrow(id); validateTaskOwnership(task); From eafe15b7cdebb730ba87720798e7b1b8fe3e93ad Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 17 Jul 2026 22:45:41 +0100 Subject: [PATCH 24/51] Move Data class from domain to application layer and turn ResponseDTO into a record --- .../in/web/dto/out/TaskResponseDTO.java | 28 ++++++++----------- .../adapters/in/web/mapper/TaskMapper.java | 2 +- .../data/TaskData.java | 2 +- .../application/service/TaskService.java | 2 +- .../ts/taskscheduler/domain/model/Task.java | 2 +- 5 files changed, 16 insertions(+), 20 deletions(-) rename src/main/java/com/javanauta/ts/taskscheduler/{domain => application}/data/TaskData.java (79%) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java index 7d1bf30..4d13ce9 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java @@ -1,24 +1,20 @@ package com.javanauta.ts.taskscheduler.adapters.in.web.dto.out; import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; -import lombok.*; +import lombok.Builder; import java.time.Instant; -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder -public class TaskResponseDTO { - - private String id; - private String name; - private String description; - private Instant creationDateTime; - private Instant scheduledDateTime; - private String userEmail; - private Instant modificationDateTime; - private NotificationStatus notificationStatus; - private String timeZoneId; +public record TaskResponseDTO( + String id, + String name, + String description, + Instant creationDateTime, + Instant scheduledDateTime, + String userEmail, + Instant modificationDateTime, + NotificationStatus notificationStatus, + String timeZoneId +) { } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/mapper/TaskMapper.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/mapper/TaskMapper.java index f03d96f..62b2f94 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/mapper/TaskMapper.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/mapper/TaskMapper.java @@ -1,6 +1,6 @@ package com.javanauta.ts.taskscheduler.adapters.in.web.mapper; -import com.javanauta.ts.taskscheduler.domain.data.TaskData; +import com.javanauta.ts.taskscheduler.application.data.TaskData; import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.out.TaskResponseDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.CreateTaskRequestDTO; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/data/TaskData.java b/src/main/java/com/javanauta/ts/taskscheduler/application/data/TaskData.java similarity index 79% rename from src/main/java/com/javanauta/ts/taskscheduler/domain/data/TaskData.java rename to src/main/java/com/javanauta/ts/taskscheduler/application/data/TaskData.java index 9c469c4..d3e9689 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/data/TaskData.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/data/TaskData.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.domain.data; +package com.javanauta.ts.taskscheduler.application.data; import lombok.Builder; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 8c1b948..34ae30b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -3,7 +3,7 @@ import com.javanauta.ts.taskscheduler.application.data.NotificationResultDetails; import com.javanauta.ts.taskscheduler.application.data.enums.NotificationResult; import com.javanauta.ts.taskscheduler.application.exception.enums.ServiceExceptionCode; -import com.javanauta.ts.taskscheduler.domain.data.TaskData; +import com.javanauta.ts.taskscheduler.application.data.TaskData; import com.javanauta.ts.taskscheduler.domain.model.Task; import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; import com.javanauta.ts.taskscheduler.ports.out.messaging.NotificationRequestPublisher; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java index 6ff3550..2539da2 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java @@ -1,6 +1,6 @@ package com.javanauta.ts.taskscheduler.domain.model; -import com.javanauta.ts.taskscheduler.domain.data.TaskData; +import com.javanauta.ts.taskscheduler.application.data.TaskData; import com.javanauta.ts.taskscheduler.domain.exception.enums.DomainExceptionCode; import com.javanauta.ts.taskscheduler.domain.exception.enums.DomainValidationExceptionCode; import com.javanauta.ts.taskscheduler.domain.model.enums.NotificationStatus; From 7ab042eadf48d852e11b44d6baec93144e8b6685 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 17 Jul 2026 22:51:22 +0100 Subject: [PATCH 25/51] Change RequestParam by PathVariable in delete/update end points for better API semantics --- .../adapters/in/web/controller/TaskController.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java index 0d7658a..2cce2ea 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java @@ -1,12 +1,12 @@ package com.javanauta.ts.taskscheduler.adapters.in.web.controller; -import com.javanauta.ts.taskscheduler.adapters.in.web.path.ApiPaths; -import com.javanauta.ts.taskscheduler.application.service.TaskService; -import com.javanauta.ts.taskscheduler.domain.model.Task; +import com.javanauta.ts.apicontract.response.SuccessResponse; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.CreateTaskRequestDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.UpdateTaskRequestDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.mapper.TaskMapper; -import com.javanauta.ts.apicontract.response.SuccessResponse; +import com.javanauta.ts.taskscheduler.adapters.in.web.path.ApiPaths; +import com.javanauta.ts.taskscheduler.application.service.TaskService; +import com.javanauta.ts.taskscheduler.domain.model.Task; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; @@ -54,7 +54,7 @@ public ResponseEntity findTasksByUserEmail() { @DeleteMapping public ResponseEntity deleteTask( - @RequestParam("id") @NotBlank String id) { + @PathVariable("/{id}") @NotBlank String id) { taskService.deleteTask(id); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); @@ -63,7 +63,7 @@ public ResponseEntity deleteTask( @PatchMapping public ResponseEntity updateTask( @RequestBody @Valid UpdateTaskRequestDTO taskDTO, - @RequestParam("id") @NotBlank String id) { + @PathVariable("/{id}") @NotBlank String id) { Task task = taskService.updateTask(taskMapper.fromUpdateTaskRequestDTO(taskDTO), id); From e2376e35f806148305f32e5cf0c2491de005e447 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 17 Jul 2026 23:04:07 +0100 Subject: [PATCH 26/51] Small fix in PathVariable syntax --- .../adapters/in/web/controller/TaskController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java index 2cce2ea..dfc89f5 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java @@ -52,18 +52,18 @@ public ResponseEntity findTasksByUserEmail() { return ResponseEntity.status(httpCode).body(successResponse); } - @DeleteMapping + @DeleteMapping("/{id}") public ResponseEntity deleteTask( - @PathVariable("/{id}") @NotBlank String id) { + @PathVariable @NotBlank String id) { taskService.deleteTask(id); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } - @PatchMapping + @PatchMapping("/{id}") public ResponseEntity updateTask( @RequestBody @Valid UpdateTaskRequestDTO taskDTO, - @PathVariable("/{id}") @NotBlank String id) { + @PathVariable @NotBlank String id) { Task task = taskService.updateTask(taskMapper.fromUpdateTaskRequestDTO(taskDTO), id); From 4dfce099ea621aaae509ca0cd67f25afce5dc43e Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Mon, 20 Jul 2026 21:59:46 +0100 Subject: [PATCH 27/51] Fix Task update logic ('save' was not being called after object update) --- .../ts/taskscheduler/application/service/TaskService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 34ae30b..b49e31a 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -53,9 +53,10 @@ public Task updateTask(TaskData taskData, String id) { Task task = getTaskOrThrow(id); validateTaskOwnership(task); task.update(taskData); + Task updatedTask = taskPersister.save(task); log.info("Task {} updated", id); - return task; + return updatedTask; } // Scheduler From d2eb6c17c5c674ed2cb08cdfbc17374740973e36 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Tue, 21 Jul 2026 22:22:35 +0100 Subject: [PATCH 28/51] Change Task.description() attribute update logic to: remains unchanged if input value is null; cleared if input value is a blank string --- .../adapters/in/web/dto/in/CreateTaskRequestDTO.java | 2 +- .../java/com/javanauta/ts/taskscheduler/domain/model/Task.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/CreateTaskRequestDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/CreateTaskRequestDTO.java index bb38c52..507aa2f 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/CreateTaskRequestDTO.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/in/CreateTaskRequestDTO.java @@ -12,7 +12,7 @@ @Jacksonized public record CreateTaskRequestDTO( @NotBlank String name, - @NotNull String description, + String description, @NotNull Instant scheduledDateTime, @NotBlank @ValidZoneId String timeZoneId) { } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java index 2539da2..1a834d1 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java @@ -71,7 +71,7 @@ public void updateStatus(NotificationStatus newStatus) { public void update(TaskData taskData) { if (taskData.name() != null) { name = taskData.name(); } - if (taskData.description() != null) { description = taskData.description(); } + if (taskData.description() != null) { description = taskData.description().isBlank() ? null : taskData.description(); } if (taskData.scheduledDateTime() != null) { scheduledDateTime = taskData.scheduledDateTime(); validateScheduledDate(); From 7b1f2e92e6c08831a137afcb192f2630a4bc483b Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 23 Jul 2026 21:33:17 +0100 Subject: [PATCH 29/51] Make AuthenticationEntryPoint also return ErrorResponse --- .../security/JwtAuthenticationEntryPoint.java | 30 +++++++++++++++++-- .../security/enums/SecurityExceptionCode.java | 18 +++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/enums/SecurityExceptionCode.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java index bd90f41..bef1009 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java @@ -1,20 +1,44 @@ package com.javanauta.ts.taskscheduler.adapters.in.security; +import com.javanauta.ts.apicontract.response.ErrorResponse; +import com.javanauta.ts.taskscheduler.adapters.in.security.enums.SecurityExceptionCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; import java.io.IOException; +import java.util.List; @Component +@RequiredArgsConstructor +@Slf4j public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("text/plain"); - response.getWriter().write(authException.getMessage()); + log.error("Authentication exception", authException); + + int httpStatusCode = HttpStatus.UNAUTHORIZED.value(); + SecurityExceptionCode exceptionCode = SecurityExceptionCode.AUTHENTICATION_ERROR; + + ErrorResponse errorResponse = new ErrorResponse( + httpStatusCode, + exceptionCode.getIdentifier(), + exceptionCode.getDefaultMessage(), + List.of() + ); + + response.setStatus(httpStatusCode); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/enums/SecurityExceptionCode.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/enums/SecurityExceptionCode.java new file mode 100644 index 0000000..5028013 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/enums/SecurityExceptionCode.java @@ -0,0 +1,18 @@ +package com.javanauta.ts.taskscheduler.adapters.in.security.enums; + +import com.javanauta.ts.taskscheduler.shared.exception.ExceptionCode; + +public enum SecurityExceptionCode implements ExceptionCode { + AUTHENTICATION_ERROR ("Authentication failed"); + + private final String defaultMessage; + + SecurityExceptionCode(String defaultMessage) { + this.defaultMessage = defaultMessage; + } + + @Override + public String getDefaultMessage() { + return defaultMessage; + } +} From 283cf81caf0fc5171f46d6bcbcfb35f7d6176918 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 23 Jul 2026 21:37:57 +0100 Subject: [PATCH 30/51] Define DTO types in SuccessResponse objects returned by end points for compilation safety and better Swagger documentation --- .../adapters/in/web/controller/TaskController.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java index dfc89f5..a430658 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java @@ -3,6 +3,7 @@ import com.javanauta.ts.apicontract.response.SuccessResponse; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.CreateTaskRequestDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.UpdateTaskRequestDTO; +import com.javanauta.ts.taskscheduler.adapters.in.web.dto.out.TaskResponseDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.mapper.TaskMapper; import com.javanauta.ts.taskscheduler.adapters.in.web.path.ApiPaths; import com.javanauta.ts.taskscheduler.application.service.TaskService; @@ -22,18 +23,17 @@ @RequiredArgsConstructor @Validated public class TaskController { - private final TaskService taskService; private final TaskMapper taskMapper; @PostMapping - public ResponseEntity createTask( + public ResponseEntity> createTask( @RequestBody @Valid CreateTaskRequestDTO createTaskRequestDTO) { Task createdTask = taskService.createTask(taskMapper.fromCreateTaskRequestDTO(createTaskRequestDTO)); HttpStatus httpCode = HttpStatus.OK; - SuccessResponse successResponse = new SuccessResponse( + SuccessResponse successResponse = new SuccessResponse<>( httpCode.value(), taskMapper.toTaskDTO(createdTask)); @@ -41,11 +41,11 @@ public ResponseEntity createTask( } @GetMapping - public ResponseEntity findTasksByUserEmail() { + public ResponseEntity>> findTasksByUserEmail() { List tasks = taskService.findTasksByUserEmail(); HttpStatus httpCode = HttpStatus.OK; - SuccessResponse successResponse = new SuccessResponse( + SuccessResponse> successResponse = new SuccessResponse<>( httpCode.value(), taskMapper.toTaskDTOList(tasks)); @@ -61,14 +61,14 @@ public ResponseEntity deleteTask( } @PatchMapping("/{id}") - public ResponseEntity updateTask( + public ResponseEntity> updateTask( @RequestBody @Valid UpdateTaskRequestDTO taskDTO, @PathVariable @NotBlank String id) { Task task = taskService.updateTask(taskMapper.fromUpdateTaskRequestDTO(taskDTO), id); HttpStatus httpCode = HttpStatus.OK; - SuccessResponse successResponse = new SuccessResponse( + SuccessResponse successResponse = new SuccessResponse<>( httpCode.value(), taskMapper.toTaskDTO(task)); From a061d0c4f5404e40c5f775d055a6c52a74d069ec Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 23 Jul 2026 22:03:16 +0100 Subject: [PATCH 31/51] Add Swagger documentation to REST Controller --- build.gradle | 1 + .../in/security/config/SecurityConfig.java | 2 + .../in/web/controller/TaskController.java | 71 ++++++++++++++++++- .../web/exception/GlobalExceptionHandler.java | 1 - 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 4a7104d..d82a6c5 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3' implementation 'io.jsonwebtoken:jjwt-api:0.13.0' implementation 'org.mapstruct:mapstruct:1.6.3' implementation 'org.mapstruct:mapstruct-processor:1.6.3' diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java index cf3ecf1..ff5af0e 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java @@ -15,6 +15,7 @@ @Configuration @EnableWebSecurity public class SecurityConfig { + public static final String SECURITY_SCHEME = "bearerAuth"; // JwtUtil and UserDetailsService instances injected by Spring private final JwtRequestFilter jwtRequestFilter; @@ -45,6 +46,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .authorizeHttpRequests(authorize -> authorize .requestMatchers("/actuator/health").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .anyRequest().authenticated() // Requires authentication for all other requests ) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java index a430658..88dd257 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java @@ -1,6 +1,8 @@ package com.javanauta.ts.taskscheduler.adapters.in.web.controller; +import com.javanauta.ts.apicontract.response.ErrorResponse; import com.javanauta.ts.apicontract.response.SuccessResponse; +import com.javanauta.ts.taskscheduler.adapters.in.security.config.SecurityConfig; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.CreateTaskRequestDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.in.UpdateTaskRequestDTO; import com.javanauta.ts.taskscheduler.adapters.in.web.dto.out.TaskResponseDTO; @@ -8,6 +10,13 @@ import com.javanauta.ts.taskscheduler.adapters.in.web.path.ApiPaths; import com.javanauta.ts.taskscheduler.application.service.TaskService; import com.javanauta.ts.taskscheduler.domain.model.Task; +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; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; @@ -20,13 +29,25 @@ @RestController @RequestMapping(ApiPaths.TASKS_V1) -@RequiredArgsConstructor +@SecurityRequirement(name = SecurityConfig.SECURITY_SCHEME) +@Tag(name = "Tasks", description = "Operations for managing the authenticated user's tasks") @Validated +@RequiredArgsConstructor public class TaskController { private final TaskService taskService; private final TaskMapper taskMapper; @PostMapping + @Operation(summary = "Create task", description = "Creates a new task for the authenticated user") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Task successfully created"), + @ApiResponse(responseCode = "401", description = "Authentication failed", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "422", description = "Request body validation failed", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) public ResponseEntity> createTask( @RequestBody @Valid CreateTaskRequestDTO createTaskRequestDTO) { @@ -41,6 +62,14 @@ public ResponseEntity> createTask( } @GetMapping + @Operation(summary = "Get user tasks", description = "Returns all tasks belonging to the authenticated user") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Tasks successfully retrieved"), + @ApiResponse(responseCode = "401", description = "Authentication failed", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) public ResponseEntity>> findTasksByUserEmail() { List tasks = taskService.findTasksByUserEmail(); @@ -53,6 +82,20 @@ public ResponseEntity>> findTasksByUserEma } @DeleteMapping("/{id}") + @Operation(summary = "Delete task", description = "Deletes a task owned by the authenticated user") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "Task successfully deleted"), + @ApiResponse(responseCode = "401", description = "Authentication failed", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "User does not own the requested task", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "Task not found", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "422", description = "Request parameter validation failed", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) public ResponseEntity deleteTask( @PathVariable @NotBlank String id) { @@ -60,7 +103,31 @@ public ResponseEntity deleteTask( return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } - @PatchMapping("/{id}") + @PatchMapping + @Operation( + summary = "Update task", + description = """ + Updates a task owned by the authenticated user. + + PATCH behavior: + > Missing fields are ignored \n\n + > Fields sent as null are ignored \n\n + > Send an empty string ("") to clear optional String fields ('description') + """ + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Task successfully updated"), + @ApiResponse(responseCode = "401", description = "Authentication failed", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "User does not own the requested task", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "Task not found", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "422", description = "Request validation failed", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) public ResponseEntity> updateTask( @RequestBody @Valid UpdateTaskRequestDTO taskDTO, @PathVariable @NotBlank String id) { diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java index 42ef100..9f04a4b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/exception/GlobalExceptionHandler.java @@ -28,7 +28,6 @@ @ControllerAdvice @Slf4j public class GlobalExceptionHandler { - private static final Map BUSINESS_CODE_HTTP_STATUS_MAP = Map.of( ServiceExceptionCode.TASK_NOT_FOUND, HttpStatus.NOT_FOUND, ServiceExceptionCode.NO_TASK_OWNERSHIP, HttpStatus.FORBIDDEN, From 5f05d3372cc0bc819d90163b9c64d82cc6afd720 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 23 Jul 2026 22:20:35 +0100 Subject: [PATCH 32/51] Fix missing path variable in PATCH end point and change GET end point's findTasksByUserEmail method name by getTasks --- .../adapters/in/web/controller/TaskController.java | 6 +++--- .../ts/taskscheduler/application/service/TaskService.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java index 88dd257..d11fcef 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/controller/TaskController.java @@ -70,8 +70,8 @@ public ResponseEntity> createTask( @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - public ResponseEntity>> findTasksByUserEmail() { - List tasks = taskService.findTasksByUserEmail(); + public ResponseEntity>> getTasks() { + List tasks = taskService.getTasks(); HttpStatus httpCode = HttpStatus.OK; SuccessResponse> successResponse = new SuccessResponse<>( @@ -103,7 +103,7 @@ public ResponseEntity deleteTask( return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } - @PatchMapping + @PatchMapping("/{id}") @Operation( summary = "Update task", description = """ diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index b49e31a..e11ddae 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -37,7 +37,7 @@ public Task createTask(TaskData taskData) { return savedTask; } - public List findTasksByUserEmail() { + public List getTasks() { return taskPersister.findByUserEmail(principalProvider.getEmail()); } From 76121564f752cf52e3697c0d33d75b5cc7e5fa3b Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 23 Jul 2026 22:45:24 +0100 Subject: [PATCH 33/51] Add missing configuration to add 'Authorize' button in swagger --- .../adapters/in/security/config/SecurityConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java index ff5af0e..9754804 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java @@ -1,6 +1,8 @@ package com.javanauta.ts.taskscheduler.adapters.in.security.config; import com.javanauta.ts.taskscheduler.adapters.in.security.JwtRequestFilter; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -14,6 +16,7 @@ @Configuration @EnableWebSecurity +@SecurityScheme(name = SecurityConfig.SECURITY_SCHEME, type = SecuritySchemeType.HTTP, bearerFormat = "JWT", scheme = "bearer") public class SecurityConfig { public static final String SECURITY_SCHEME = "bearerAuth"; From d8e148f192efd85476c6ddccd5ced0717dff47c3 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 26 Jul 2026 21:22:36 +0100 Subject: [PATCH 34/51] Refactor security so there is no more user credentials authentication and only HttpHeaders check (that will be filled and passed along by the Gateway) --- build.gradle | 2 +- .../in/security/JwtRequestFilter.java | 73 ------------------- .../adapters/in/security/JwtUtil.java | 56 -------------- .../in/security/UserDetailsServiceImpl.java | 25 ------- .../AuthenticatedPrincipal.java | 9 +++ .../ForwardedIdentityFilter.java | 49 +++++++++++++ .../in/security/config/SecurityConfig.java | 44 ++++------- .../JwtAuthenticationEntryPoint.java | 4 +- .../enums/SecurityExceptionCode.java | 2 +- .../SpringSecurityPrincipalProvider.java | 26 ------- .../SecurityContextPrincipalProvider.java | 41 +++++++++++ .../ports/out/security/PrincipalProvider.java | 3 + 12 files changed, 120 insertions(+), 214 deletions(-) delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtRequestFilter.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/authentication/AuthenticatedPrincipal.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/authentication/ForwardedIdentityFilter.java rename src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/{ => exception}/JwtAuthenticationEntryPoint.java (90%) rename src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/{ => exception}/enums/SecurityExceptionCode.java (84%) delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityPrincipalProvider.java create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/context/SecurityContextPrincipalProvider.java diff --git a/build.gradle b/build.gradle index d82a6c5..03d4b5a 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,7 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.springframework.boot:spring-boot-starter-amqp' implementation 'com.github.enzobbom:ts-events:v1.0.0' - implementation 'com.github.enzobbom:ts-api-contract:v1.0.0' + implementation 'com.github.enzobbom:ts-api-contract:v1.1.0' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtRequestFilter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtRequestFilter.java deleted file mode 100644 index 2c8dcf2..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtRequestFilter.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.security; - -import feign.FeignException; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -// Defines the JwtRequestFilter class, which extends OncePerRequestFilter -@Component -public class JwtRequestFilter extends OncePerRequestFilter { - - // Defines properties to store instances of JwtUtil and UserDetailsService - private final JwtUtil jwtUtil; - private final UserDetailsServiceImpl userDetailsService; - - // Constructor that initializes the properties with provided instances - public JwtRequestFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) { - this.jwtUtil = jwtUtil; - this.userDetailsService = userDetailsService; - } - - // Method called once per request to process the filter - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - - // Gets the value of the "Authorization" header from the request - final String authorizationHeader = request.getHeader("Authorization"); - - // Checks whether the header exists and starts with "Bearer " - if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { - // Extracts the JWT token from the header - final String token = authorizationHeader.substring(7); - String username; - - try { - // Extracts the username from the JWT token - username = jwtUtil.extractUsername(token); - } catch (Exception e) { - throw new AuthenticationServiceException("Invalid JWT token"); - } - - // If the username is not null and the user is not yet authenticated - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - // Validate the token - if (!jwtUtil.validateToken(token, username)) { - throw new AuthenticationServiceException("JWT token validation failed"); - } - - UserDetails userDetails; - try { - userDetails = userDetailsService.loadUserDetails(username, authorizationHeader); - } catch (FeignException e) { - throw new AuthenticationServiceException("Failed to load user info from User service", e); - } - - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(authentication); - } - } - - // Continues the filter chain, allowing the request to proceed - chain.doFilter(request, response); - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java deleted file mode 100644 index 3bed5d3..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtUtil.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.security; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; -import java.util.Date; - -@Component -public class JwtUtil { - - private JwtParser jwtParser; - - // Secret key used to sign and verify JWT tokens - @Value("${ts.jwt.secret}") - private String secretKey; - - @PostConstruct - public void init() { - jwtParser = Jwts.parser() - .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) - .build(); - } - - // Extracts claims from the JWT token - public Claims extractClaims(String token) { - return jwtParser - .parseSignedClaims(token) // Parses the JWT token and gets claims - .getPayload(); // Returns claims body - } - - // Extracts username from the JWT token - public String extractUsername(String token) { - // Gets the subject (username) from the claims - return extractClaims(token).getSubject(); - } - - // Checks whether the JWT token is expired - public boolean isTokenExpired(String token) { - // Compares token expiration date with current date - return extractClaims(token).getExpiration().before(new Date()); - } - - // Validates the JWT token by checking username match and expiration - public boolean validateToken(String token, String username) { - // Extracts username from the token - final String extractedUsername = extractUsername(token); - // Checks if username matches and token is not expired - return (extractedUsername.equals(username) && !isTokenExpired(token)); - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java deleted file mode 100644 index 34139a1..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/UserDetailsServiceImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.in.security; - -import com.javanauta.ts.taskscheduler.adapters.out.client.user.dto.ExternalUserDTO; -import com.javanauta.ts.taskscheduler.ports.out.client.user.ExternalUserProvider; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -@Component -public class UserDetailsServiceImpl { - - private final ExternalUserProvider externalUserProvider; - - public UserDetailsServiceImpl(ExternalUserProvider externalUserProvider) { - this.externalUserProvider = externalUserProvider; - } - - public UserDetails loadUserDetails(String email, String token) { - ExternalUserDTO externalUserDTO = externalUserProvider.getUserByEmail(email, token); - - return org.springframework.security.core.userdetails.User - .withUsername(externalUserDTO.getEmail()) // Sets the username as the email - .password(externalUserDTO.getPassword()) // Sets the user's password - .build(); // Builds the UserDetails object - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/authentication/AuthenticatedPrincipal.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/authentication/AuthenticatedPrincipal.java new file mode 100644 index 0000000..217a1ab --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/authentication/AuthenticatedPrincipal.java @@ -0,0 +1,9 @@ +package com.javanauta.ts.taskscheduler.adapters.in.security.authentication; + +import java.util.UUID; + +public record AuthenticatedPrincipal( + UUID id, + String email +) { +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/authentication/ForwardedIdentityFilter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/authentication/ForwardedIdentityFilter.java new file mode 100644 index 0000000..3d9e226 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/authentication/ForwardedIdentityFilter.java @@ -0,0 +1,49 @@ +package com.javanauta.ts.taskscheduler.adapters.in.security.authentication; + +import com.javanauta.ts.apicontract.http.HttpHeaders; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; +import java.util.UUID; + +@Component +public class ForwardedIdentityFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String userId = request.getHeader(HttpHeaders.USER_ID); + String email = request.getHeader(HttpHeaders.USER_EMAIL); + + if (userId != null && email != null) { + AuthenticatedPrincipal principal = + new AuthenticatedPrincipal( + UUID.fromString(userId), + email + ); + + Authentication authentication = + new UsernamePasswordAuthenticationToken( + principal, + null, + Collections.emptyList() + ); + + SecurityContextHolder + .getContext() + .setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java index 9754804..d76a947 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java @@ -1,9 +1,9 @@ package com.javanauta.ts.taskscheduler.adapters.in.security.config; -import com.javanauta.ts.taskscheduler.adapters.in.security.JwtRequestFilter; +import com.javanauta.ts.taskscheduler.adapters.in.security.authentication.ForwardedIdentityFilter; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -17,45 +17,29 @@ @Configuration @EnableWebSecurity @SecurityScheme(name = SecurityConfig.SECURITY_SCHEME, type = SecuritySchemeType.HTTP, bearerFormat = "JWT", scheme = "bearer") +@RequiredArgsConstructor public class SecurityConfig { public static final String SECURITY_SCHEME = "bearerAuth"; + private final ForwardedIdentityFilter forwardedIdentityFilter; + private final AuthenticationEntryPoint authenticationEntryPoint; - // JwtUtil and UserDetailsService instances injected by Spring - private final JwtRequestFilter jwtRequestFilter; - private final AuthenticationEntryPoint jwtAuthenticationEntryPoint; - - // Constructor for dependency injection of JwtUtil and UserDetailsService - @Autowired - public SecurityConfig(JwtRequestFilter jwtRequestFilter, AuthenticationEntryPoint jwtAuthenticationEntryPoint) { - this.jwtRequestFilter = jwtRequestFilter; - this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; - - } - - // Security filter configuration @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) { - // Creates a JwtRequestFilter instance using JwtUtil and UserDetailsService - - http - .csrf(AbstractHttpConfigurer::disable) // Disables CSRF protection for REST APIs (not needed in stateless APIs) - + return http + .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Sets session policy to stateless + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) - - .exceptionHandling(ex -> ex - .authenticationEntryPoint(jwtAuthenticationEntryPoint)) - .authorizeHttpRequests(authorize -> authorize .requestMatchers("/actuator/health").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .anyRequest().authenticated() // Requires authentication for all other requests ) - - .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // Adds JWT filter before default authentication filter - - // Returns the built security configuration - return http.build(); + .exceptionHandling(ex -> ex + .authenticationEntryPoint(authenticationEntryPoint)) + .addFilterBefore( + forwardedIdentityFilter, + UsernamePasswordAuthenticationFilter.class) + .build(); } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/exception/JwtAuthenticationEntryPoint.java similarity index 90% rename from src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/exception/JwtAuthenticationEntryPoint.java index bef1009..505ed5c 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/exception/JwtAuthenticationEntryPoint.java @@ -1,7 +1,7 @@ -package com.javanauta.ts.taskscheduler.adapters.in.security; +package com.javanauta.ts.taskscheduler.adapters.in.security.exception; import com.javanauta.ts.apicontract.response.ErrorResponse; -import com.javanauta.ts.taskscheduler.adapters.in.security.enums.SecurityExceptionCode; +import com.javanauta.ts.taskscheduler.adapters.in.security.exception.enums.SecurityExceptionCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/enums/SecurityExceptionCode.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/exception/enums/SecurityExceptionCode.java similarity index 84% rename from src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/enums/SecurityExceptionCode.java rename to src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/exception/enums/SecurityExceptionCode.java index 5028013..161f54e 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/enums/SecurityExceptionCode.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/exception/enums/SecurityExceptionCode.java @@ -1,4 +1,4 @@ -package com.javanauta.ts.taskscheduler.adapters.in.security.enums; +package com.javanauta.ts.taskscheduler.adapters.in.security.exception.enums; import com.javanauta.ts.taskscheduler.shared.exception.ExceptionCode; diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityPrincipalProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityPrincipalProvider.java deleted file mode 100644 index 20f76a3..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/SpringSecurityPrincipalProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.out.security; - -import com.javanauta.ts.taskscheduler.ports.out.security.PrincipalProvider; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; - -@Component -public class SpringSecurityPrincipalProvider implements PrincipalProvider { - - @Override - public String getEmail() { - Authentication authentication = SecurityContextHolder - .getContext() - .getAuthentication(); - - if (authentication == null || - !authentication.isAuthenticated() || - "anonymousUser".equals(authentication.getPrincipal())) { - - throw new IllegalStateException("No authenticated user found"); - } - - return authentication.getName(); - } -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/context/SecurityContextPrincipalProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/context/SecurityContextPrincipalProvider.java new file mode 100644 index 0000000..9b166e2 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/context/SecurityContextPrincipalProvider.java @@ -0,0 +1,41 @@ +package com.javanauta.ts.taskscheduler.adapters.out.security.context; + +import com.javanauta.ts.taskscheduler.adapters.in.security.authentication.AuthenticatedPrincipal; +import com.javanauta.ts.taskscheduler.ports.out.security.PrincipalProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +public class SecurityContextPrincipalProvider implements PrincipalProvider { + + @Override + public UUID getUserId() { + return getAuthenticatedUser().id(); + } + + @Override + public String getEmail() { + return getAuthenticatedUser().email(); + } + + private AuthenticatedPrincipal getAuthenticatedUser() { + Authentication authentication = + SecurityContextHolder + .getContext() + .getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("No authenticated user found"); + } + + Object principal = authentication.getPrincipal(); + if (!(principal instanceof AuthenticatedPrincipal user)) { + throw new IllegalStateException("Invalid authenticated principal"); + } + + return user; + } +} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java index 90da7a4..0e0ec1e 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java @@ -1,5 +1,8 @@ package com.javanauta.ts.taskscheduler.ports.out.security; +import java.util.UUID; + public interface PrincipalProvider { + UUID getUserId(); String getEmail(); } \ No newline at end of file From af37e894c297e5470938bd5061aa612c85917841 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 26 Jul 2026 21:26:22 +0100 Subject: [PATCH 35/51] Remove now unnecessary Feign dependency and implementations (call to User service to get credentials replaced by JWT authentication on Gateway) --- build.gradle | 1 - .../TaskSchedulerApplication.java | 2 -- .../user/ExternalUserProviderAdapter.java | 18 ------------------ .../out/client/user/FeignUserClient.java | 14 -------------- .../out/client/user/dto/ExternalUserDTO.java | 13 ------------- .../out/client/user/ExternalUserProvider.java | 7 ------- 6 files changed, 55 deletions(-) delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/ExternalUserProviderAdapter.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/FeignUserClient.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/dto/ExternalUserDTO.java delete mode 100644 src/main/java/com/javanauta/ts/taskscheduler/ports/out/client/user/ExternalUserProvider.java diff --git a/build.gradle b/build.gradle index 03d4b5a..e0196ed 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3' implementation 'io.jsonwebtoken:jjwt-api:0.13.0' implementation 'org.mapstruct:mapstruct:1.6.3' diff --git a/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java b/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java index fbab108..3321a2b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java @@ -4,11 +4,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration; -import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication(exclude = MongoAutoConfiguration.class) -@EnableFeignClients @EnableScheduling @EnableRabbit public class TaskSchedulerApplication { diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/ExternalUserProviderAdapter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/ExternalUserProviderAdapter.java deleted file mode 100644 index 2bc3e7f..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/ExternalUserProviderAdapter.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.out.client.user; - -import com.javanauta.ts.taskscheduler.adapters.out.client.user.dto.ExternalUserDTO; -import com.javanauta.ts.taskscheduler.ports.out.client.user.ExternalUserProvider; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class ExternalUserProviderAdapter implements ExternalUserProvider { - private final FeignUserClient userProvider; - - @Override - public ExternalUserDTO getUserByEmail(String email, String token) { - return userProvider.getUserByEmail(email, token); - } -} - diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/FeignUserClient.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/FeignUserClient.java deleted file mode 100644 index 4c430f6..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/FeignUserClient.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.out.client.user; - -import com.javanauta.ts.taskscheduler.adapters.out.client.user.dto.ExternalUserDTO; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient(name = "user", url = "${ts.user.service.uri}") -public interface FeignUserClient { - - @GetMapping - ExternalUserDTO getUserByEmail(@RequestParam("email") String email, @RequestHeader("Authorization") String token); -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/dto/ExternalUserDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/dto/ExternalUserDTO.java deleted file mode 100644 index 757debd..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/client/user/dto/ExternalUserDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.javanauta.ts.taskscheduler.adapters.out.client.user.dto; - -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ExternalUserDTO { - private String email; - private String password; -} diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/client/user/ExternalUserProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/client/user/ExternalUserProvider.java deleted file mode 100644 index 9dbd6c1..0000000 --- a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/client/user/ExternalUserProvider.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.javanauta.ts.taskscheduler.ports.out.client.user; - -import com.javanauta.ts.taskscheduler.adapters.out.client.user.dto.ExternalUserDTO; - -public interface ExternalUserProvider { - ExternalUserDTO getUserByEmail(String email, String token); -} From 8ef3423ee245e8efc59f5b86f8770ace897cf88b Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 26 Jul 2026 22:13:51 +0100 Subject: [PATCH 36/51] Introduce userId to Task entity --- .../adapters/in/web/dto/out/TaskResponseDTO.java | 2 ++ .../context/SecurityContextPrincipalProvider.java | 2 +- .../ts/taskscheduler/application/service/TaskService.java | 6 ++++-- .../com/javanauta/ts/taskscheduler/domain/model/Task.java | 8 ++++++-- .../ports/out/security/PrincipalProvider.java | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java index 4d13ce9..d030d08 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/dto/out/TaskResponseDTO.java @@ -4,6 +4,7 @@ import lombok.Builder; import java.time.Instant; +import java.util.UUID; @Builder public record TaskResponseDTO( @@ -12,6 +13,7 @@ public record TaskResponseDTO( String description, Instant creationDateTime, Instant scheduledDateTime, + UUID userId, String userEmail, Instant modificationDateTime, NotificationStatus notificationStatus, diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/context/SecurityContextPrincipalProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/context/SecurityContextPrincipalProvider.java index 9b166e2..10f2b52 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/context/SecurityContextPrincipalProvider.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/security/context/SecurityContextPrincipalProvider.java @@ -12,7 +12,7 @@ public class SecurityContextPrincipalProvider implements PrincipalProvider { @Override - public UUID getUserId() { + public UUID getId() { return getAuthenticatedUser().id(); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index e11ddae..7501e16 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -28,8 +28,10 @@ public class TaskService { // Controller public Task createTask(TaskData taskData) { - String userEmail = principalProvider.getEmail(); - Task task = Task.create(taskData, userEmail); + Task task = Task.create( + taskData, + principalProvider.getId(), + principalProvider.getEmail()); Task savedTask = taskPersister.save(task); log.info("Task {} created", savedTask.getId()); diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java index 1a834d1..01777eb 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java @@ -15,6 +15,7 @@ import java.time.Instant; import java.time.ZoneId; import java.util.List; +import java.util.UUID; @Getter @Setter @@ -34,6 +35,7 @@ public class Task { private Instant creationDateTime; private Instant scheduledDateTime; @Indexed + private UUID userId; private String userEmail; private Instant modificationDateTime; private NotificationStatus notificationStatus; @@ -41,10 +43,11 @@ public class Task { private final static String SCHEDULED_DATETIME_FIELD_NAME = "scheduledDateTime"; - private Task(String name, String description, Instant scheduledDateTime, String userEmail, ZoneId timeZoneId) { + private Task(String name, String description, Instant scheduledDateTime, UUID userId, String userEmail, ZoneId timeZoneId) { this.name = name; this.description = description; this.scheduledDateTime = scheduledDateTime; + this.userId = userId; this.userEmail = userEmail; this.timeZoneId = timeZoneId; @@ -54,11 +57,12 @@ private Task(String name, String description, Instant scheduledDateTime, String validateScheduledDate(); } - public static Task create(TaskData taskData, String userEmail) { + public static Task create(TaskData taskData, UUID userId, String userEmail) { return new Task( taskData.name(), taskData.description(), taskData.scheduledDateTime(), + userId, userEmail, taskData.timeZoneId()); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java index 0e0ec1e..c203c65 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/security/PrincipalProvider.java @@ -3,6 +3,6 @@ import java.util.UUID; public interface PrincipalProvider { - UUID getUserId(); + UUID getId(); String getEmail(); } \ No newline at end of file From 441ef42d58888c01b4f8c3a0792eb1f45c388b38 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 26 Jul 2026 22:19:34 +0100 Subject: [PATCH 37/51] Make 'getTasks()' method use userId instead of userEmail (for cases where user changes email) --- .../adapters/out/persistence/MongoTaskRepository.java | 3 ++- .../adapters/out/persistence/TaskPersisterAdapter.java | 5 +++-- .../ts/taskscheduler/application/service/TaskService.java | 4 ++-- .../taskscheduler/ports/out/persistence/TaskPersister.java | 3 ++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java index d6a46f1..bc9dc4b 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/MongoTaskRepository.java @@ -8,9 +8,10 @@ import java.time.Instant; import java.util.Collection; import java.util.List; +import java.util.UUID; @Repository public interface MongoTaskRepository extends MongoRepository { List findByNotificationStatusInAndScheduledDateTimeBetween(Collection statuses, Instant initialDateTime, Instant finalDateTime); - List findByUserEmail(String userEmail); + List findByUserId(UUID userId); } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java index 21048af..70d85d3 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/TaskPersisterAdapter.java @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.UUID; @Component @RequiredArgsConstructor @@ -37,7 +38,7 @@ public List findByNotificationStatusInAndScheduledDateTimeBetween(Collecti } @Override - public List findByUserEmail(String userEmail) { - return taskRepository.findByUserEmail(userEmail); + public List findByUserId(UUID userId) { + return taskRepository.findByUserId(userId); } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java index 7501e16..5265e00 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/application/service/TaskService.java @@ -40,7 +40,7 @@ public Task createTask(TaskData taskData) { } public List getTasks() { - return taskPersister.findByUserEmail(principalProvider.getEmail()); + return taskPersister.findByUserId(principalProvider.getId()); } public void deleteTask(String id) { @@ -113,7 +113,7 @@ private Task getTaskOrThrow(String id) { } private void validateTaskOwnership(Task task) { - if (!task.getUserEmail().equals(principalProvider.getEmail())) { + if (!task.getUserId().equals(principalProvider.getId())) { throw new ApplicationException(ServiceExceptionCode.NO_TASK_OWNERSHIP); } } diff --git a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java index 406dabf..fa6fa00 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/ports/out/persistence/TaskPersister.java @@ -7,11 +7,12 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.UUID; public interface TaskPersister { Task save(Task task); void deleteById(String id); Optional findById(String id); List findByNotificationStatusInAndScheduledDateTimeBetween(Collection statuses, Instant initialDateTime, Instant finalDateTime); - List findByUserEmail(String userEmail); + List findByUserId(UUID userId); } From 658e7789558b874e34dc34532773af6b2f839357 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Wed, 29 Jul 2026 21:32:03 +0100 Subject: [PATCH 38/51] Adjustmets for proper Gateway routing of Swagger end points --- .../adapters/in/web/config/OpenApiConfig.java | 22 +++++++++++++++++++ src/main/resources/application.properties | 1 + 2 files changed, 23 insertions(+) create mode 100644 src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/config/OpenApiConfig.java diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/config/OpenApiConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/config/OpenApiConfig.java new file mode 100644 index 0000000..e35ed99 --- /dev/null +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/web/config/OpenApiConfig.java @@ -0,0 +1,22 @@ +package com.javanauta.ts.taskscheduler.adapters.in.web.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI taskOpenAPI() { + return new OpenAPI() + .servers(List.of(new Server().url("/"))) + .info(new Info() + .title("Task Service API") + .version("1.0.0")); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0e96e59..799cf46 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,7 @@ spring.application.name=task-scheduler spring.application.description=Task Scheduler - Task scheduler module +server.forward-headers-strategy = framework server.port=8081 spring.data.mongodb.database=ts_task_scheduler From d3e5035a6fa38bc399f586ad30c7e971af0103c2 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Thu, 30 Jul 2026 21:54:02 +0100 Subject: [PATCH 39/51] Fix MongoConfig Bean dependency injection and add uuid-representation for saving the User UUID to the database --- .../out/persistence/config/MongoConfig.java | 28 +++++++++++++++---- src/main/resources/application.properties | 1 + 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java index 4b81e98..ab76c75 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java @@ -2,12 +2,15 @@ import com.javanauta.ts.taskscheduler.adapters.out.persistence.converter.ZoneIdReadConverter; import com.javanauta.ts.taskscheduler.adapters.out.persistence.converter.ZoneIdWriteConverter; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; +import lombok.extern.slf4j.Slf4j; +import org.bson.UuidRepresentation; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import lombok.extern.slf4j.Slf4j; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; @@ -20,14 +23,29 @@ public class MongoConfig { @Bean - public MongoClient mongoClient(@Value("${spring.data.mongodb.uri}") String uri) { + public MongoClient mongoClient( + @Value("${spring.data.mongodb.uri}") String uri, + @Value("${spring.data.mongodb.uuid-representation}") UuidRepresentation uuidRepresentation) { + log.info("Using MongoDB URI: {}", uri); - return MongoClients.create(uri); + + MongoClientSettings mongoClientSettings = MongoClientSettings.builder() + .uuidRepresentation(uuidRepresentation) + .applyConnectionString(new ConnectionString(uri)) + .build(); + + return MongoClients.create(mongoClientSettings); } @Bean - public MongoDatabaseFactory mongoDatabaseFactory(@Value("${spring.data.mongodb.uri}") String uri) { - return new SimpleMongoClientDatabaseFactory(uri); + public MongoDatabaseFactory mongoDatabaseFactory( + MongoClient mongoClient, + @Value("${spring.data.mongodb.database}") String databaseName) { + + return new SimpleMongoClientDatabaseFactory( + mongoClient, + databaseName + ); } @Bean diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 799cf46..d4cb684 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,6 +5,7 @@ server.forward-headers-strategy = framework server.port=8081 spring.data.mongodb.database=ts_task_scheduler +spring.data.mongodb.uuid-representation=standard ts.scheduler.frequency=0 */2 * * * * From 4bc5950dd565bdb2969f0d80281f557b097e741a Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 31 Jul 2026 12:05:11 +0100 Subject: [PATCH 40/51] Clean up and organize application properties --- .env.example | 9 --------- .../out/persistence/config/MongoConfig.java | 2 -- src/main/resources/application.properties | 15 +++------------ 3 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index ea5a97e..0000000 --- a/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -############################################ -# Task Scheduler - Task Scheduler module -# Required Environment Variables -# -# Note: use double quotes only if the value contains spaces or starts with '#'. -############################################ - -# Secret key used to sign and verify JWT tokens -TS_JWT_SECRET=your-secret-key \ No newline at end of file diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java index ab76c75..0948773 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/out/persistence/config/MongoConfig.java @@ -27,8 +27,6 @@ public MongoClient mongoClient( @Value("${spring.data.mongodb.uri}") String uri, @Value("${spring.data.mongodb.uuid-representation}") UuidRepresentation uuidRepresentation) { - log.info("Using MongoDB URI: {}", uri); - MongoClientSettings mongoClientSettings = MongoClientSettings.builder() .uuidRepresentation(uuidRepresentation) .applyConnectionString(new ConnectionString(uri)) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d4cb684..bf49a7c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,23 +4,14 @@ spring.application.description=Task Scheduler - Task scheduler module server.forward-headers-strategy = framework server.port=8081 -spring.data.mongodb.database=ts_task_scheduler -spring.data.mongodb.uuid-representation=standard - ts.scheduler.frequency=0 */2 * * * * management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true -# If environment variables are not defined, localhost defaults will be used (currently, these env vars are only set by -# the System repo's docker compose to run the application in Docker) -spring.data.mongodb.uri=${SPRING_DATA_MONGODB_URI:mongodb://localhost:27017/ts_task_scheduler} -ts.user.service.uri=${TS_USER_SERVICE_URI:http://localhost:8080/user} - -# Secret key used to sign and verify JWT tokens (injected via environment variables) -ts.jwt.secret=${TS_JWT_SECRET} - -# AMQP configuration +spring.data.mongodb.uri=${SPRING_DATA_MONGODB_URI:mongodb://localhost:27017} +spring.data.mongodb.database=ts_task_scheduler +spring.data.mongodb.uuid-representation=standard spring.rabbitmq.host=${SPRING_RABBITMQ_HOST:localhost} spring.rabbitmq.port=${SPRING_RABBITMQ_PORT:5672} From c29d82a93c767d1110c5f908198762ca9382d947 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 31 Jul 2026 21:37:14 +0100 Subject: [PATCH 41/51] Final application properties refactor before first deploy --- src/main/resources/application.properties | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bf49a7c..71ccada 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,17 +4,17 @@ spring.application.description=Task Scheduler - Task scheduler module server.forward-headers-strategy = framework server.port=8081 -ts.scheduler.frequency=0 */2 * * * * +ts.scheduler.frequency=${TS_SCHEDULER_FREQUENCY:0 */2 * * * *} management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true -spring.data.mongodb.uri=${SPRING_DATA_MONGODB_URI:mongodb://localhost:27017} +spring.data.mongodb.uri=mongodb://${TS_MONGO_USER:dev_user}:${TS_MONGO_PASSWORD:devpass}@${TS_MONGO_HOST:localhost}:27017?authSource=admin spring.data.mongodb.database=ts_task_scheduler spring.data.mongodb.uuid-representation=standard -spring.rabbitmq.host=${SPRING_RABBITMQ_HOST:localhost} -spring.rabbitmq.port=${SPRING_RABBITMQ_PORT:5672} -spring.rabbitmq.username=${SPRING_RABBITMQ_USERNAME:guest} -spring.rabbitmq.password=${SPRING_RABBITMQ_PASSWORD:guest} -spring.rabbitmq.virtual-host=${SPRING_RABBITMQ_VIRTUAL_HOST:/} +spring.rabbitmq.host=${TS_RABBITMQ_HOST:localhost} +spring.rabbitmq.port=5672 +spring.rabbitmq.username=${TS_RABBITMQ_TASK_USER:guest} +spring.rabbitmq.password=${TS_RABBITMQ_TASK_PASSWORD:guest} +spring.rabbitmq.virtual-host=${TS_RABBITMQ_VHOST:/} From 96db541529e8754e523b08ef507700690905f46d Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 2 Aug 2026 13:09:38 +0100 Subject: [PATCH 42/51] Rename service to 'task' --- Dockerfile | 4 ++-- build.gradle | 2 +- settings.gradle | 2 +- src/main/resources/application.properties | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3d8d515..396a32d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,8 @@ COPY . . RUN gradle build --no-daemon FROM eclipse-temurin:17-jdk-jammy WORKDIR /app -COPY --from=build /app/build/libs/*.jar /app/task-scheduler.jar +COPY --from=build /app/build/libs/*.jar /app/task.jar EXPOSE 8081 -CMD ["java", "-jar", "/app/task-scheduler.jar"] +CMD ["java", "-jar", "/app/task.jar"] diff --git a/build.gradle b/build.gradle index e0196ed..7a25366 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ tasks.named('test') { sonarqube { properties { - property 'sonar.projectKey', 'task-scheduler' + property 'sonar.projectKey', 'task' property 'sonar.host.url', 'http://localhost:9000' property 'sonar.login', 'squ_dc76e8eeb6ed2d1dfc066db1af123d8dde47f3f6' } diff --git a/settings.gradle b/settings.gradle index 1e7f786..70f4251 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -rootProject.name = 'task-scheduler' +rootProject.name = 'task' dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 71ccada..9effa31 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ -spring.application.name=task-scheduler -spring.application.description=Task Scheduler - Task scheduler module +spring.application.name=task +spring.application.description=Task Scheduler - Task module server.forward-headers-strategy = framework server.port=8081 From 2a67798b16226a0c19465d24c2ba0e70c72b9760 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 2 Aug 2026 13:11:26 +0100 Subject: [PATCH 43/51] Rename env var --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9effa31..88d9779 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,7 +4,7 @@ spring.application.description=Task Scheduler - Task module server.forward-headers-strategy = framework server.port=8081 -ts.scheduler.frequency=${TS_SCHEDULER_FREQUENCY:0 */2 * * * *} +ts.scheduler.frequency=${TS_TASK_NOTIFICATION_TRIGGER_FREQUENCY:0 */2 * * * *} management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true From 95f5d2f5bda640c03ab35defe611657e859274cf Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 2 Aug 2026 13:16:50 +0100 Subject: [PATCH 44/51] Missing change regarding service rename --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 88d9779..60451b3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,7 +10,7 @@ management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true spring.data.mongodb.uri=mongodb://${TS_MONGO_USER:dev_user}:${TS_MONGO_PASSWORD:devpass}@${TS_MONGO_HOST:localhost}:27017?authSource=admin -spring.data.mongodb.database=ts_task_scheduler +spring.data.mongodb.database=ts_task spring.data.mongodb.uuid-representation=standard spring.rabbitmq.host=${TS_RABBITMQ_HOST:localhost} From bf6afbd459b0244c56c22d5f4efeaf729615f8c0 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 2 Aug 2026 13:21:45 +0100 Subject: [PATCH 45/51] Rename Task model Document name to plural (as it's actually the name of the Collection and not of a single Document) --- .../java/com/javanauta/ts/taskscheduler/domain/model/Task.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java index 01777eb..93e4465 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/domain/model/Task.java @@ -22,7 +22,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -@Document("task") +@Document("tasks") @CompoundIndex( name = "notification_schedule_idx", def = "{'notificationStatus': 1, 'scheduledDateTime': 1}" From 7a73ba3707d4fb1b479425f3e159166c9d42c3d1 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 2 Aug 2026 13:44:20 +0100 Subject: [PATCH 46/51] (Hopefully) last renaming change --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7a25366..1db5825 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { group = 'com.javanauta.ts' version = '0.0.1-SNAPSHOT' -description = 'Task Scheduler - Task scheduler module' +description = 'Task Scheduler - Task module' java { toolchain { From 95d4b065c6ef3cecc49acdb851b2ccd4c26d9700 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Sun, 2 Aug 2026 16:33:57 +0100 Subject: [PATCH 47/51] Build and docker adjustments --- .dockerignore | 8 ++++++++ Dockerfile | 46 +++++++++++++++++++++++++++++++--------------- build.gradle | 4 ++++ 3 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5c8d574 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.gradle +build +out +.idea +.git +*.iml +docker-compose*.yml +README.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 396a32d..a0062bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,37 @@ -# Basic Docker file -#FROM eclipse-temurin:17-jdk-jammy -#WORKDIR /app -#COPY build/libs/task-scheduler-0.0.1-SNAPSHOT.jar /app/task-scheduler.jar -#EXPOSE 8081 -#CMD ["java", "-jar", "/app/task-scheduler.jar"] - -# Build +### Build stage + +# creates a build image stage with Gradle and JDK FROM gradle:jdk17 AS build + +# sets the working directory inside the image stage WORKDIR /app -COPY . . -# Run .jar -RUN gradle build --no-daemon -FROM eclipse-temurin:17-jdk-jammy +# copies build files and Gradle wrapper to the image stage +COPY build.gradle settings.gradle gradlew ./ +COPY gradle gradle + +# downloads dependencies and caches them in a Docker image layer +RUN ./gradlew dependencies --no-daemon || true + +# copies source code to the image stage +COPY src src + +# runs a temporary build container, builds the executable .jar, and saves the generated files as a new image layer +RUN ./gradlew bootJar --no-daemon + +### Runtime stage + +# creates a smaller runtime image stage with only the JRE +FROM eclipse-temurin:17-jre-jammy + +# sets the working directory inside the runtime image WORKDIR /app -COPY --from=build /app/build/libs/*.jar /app/task.jar -EXPOSE 8081 -CMD ["java", "-jar", "/app/task.jar"] +# copies the built .jar file from the build stage into the runtime image +COPY --from=build /app/build/libs/*.jar ts-task.jar + +# documents the port the application container listens on +EXPOSE 8080 +# defines the command executed when the application container starts +ENTRYPOINT ["java", "-jar", "/app/ts-task.jar"] diff --git a/build.gradle b/build.gradle index 1db5825..6dc5a0e 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,10 @@ dependencyManagement { } } +tasks.named('jar') { + enabled = false +} + tasks.named('test') { useJUnitPlatform() } From 8746b258cc24875bb03101d0af198a4463411a05 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Tue, 4 Aug 2026 15:05:13 +0100 Subject: [PATCH 48/51] Adjust build.gradle version, permit all actuator health check end points and permit Spring's /error end point to avoid unexpected errors generating a 401 response --- build.gradle | 2 +- .../adapters/in/security/config/SecurityConfig.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6dc5a0e..08bca14 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'com.javanauta.ts' -version = '0.0.1-SNAPSHOT' +version = '1.0.0-SNAPSHOT' description = 'Task Scheduler - Task module' java { diff --git a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java index d76a947..14937dd 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/adapters/in/security/config/SecurityConfig.java @@ -31,8 +31,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/actuator/health").permitAll() + .requestMatchers("/actuator/health/**").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/error").permitAll() .anyRequest().authenticated() // Requires authentication for all other requests ) .exceptionHandling(ex -> ex From 882d4cb90095b73ac25527a9819615ced9301a24 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Wed, 5 Aug 2026 22:19:19 +0100 Subject: [PATCH 49/51] Final env vars adjustments --- src/main/resources/application.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 60451b3..77cbafd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,12 +9,12 @@ ts.scheduler.frequency=${TS_TASK_NOTIFICATION_TRIGGER_FREQUENCY:0 */2 * * * *} management.endpoints.web.exposure.include=health management.endpoint.health.probes.enabled=true -spring.data.mongodb.uri=mongodb://${TS_MONGO_USER:dev_user}:${TS_MONGO_PASSWORD:devpass}@${TS_MONGO_HOST:localhost}:27017?authSource=admin -spring.data.mongodb.database=ts_task +spring.data.mongodb.uri=mongodb://${TS_MONGO_USER:dev_user}:${TS_MONGO_PASSWORD:devpass}@${TS_MONGO_HOST:localhost}:${TS_MONGO_PORT:27017}?authSource=admin +spring.data.mongodb.database=${TS_MONGO_DB:ts_task} spring.data.mongodb.uuid-representation=standard spring.rabbitmq.host=${TS_RABBITMQ_HOST:localhost} -spring.rabbitmq.port=5672 +spring.rabbitmq.port=${TS_RABBITMQ_PORT:5672} spring.rabbitmq.username=${TS_RABBITMQ_TASK_USER:guest} spring.rabbitmq.password=${TS_RABBITMQ_TASK_PASSWORD:guest} spring.rabbitmq.virtual-host=${TS_RABBITMQ_VHOST:/} From d74a0c123ff56700ed5f075cfcb809ac92de26f0 Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Wed, 5 Aug 2026 22:20:39 +0100 Subject: [PATCH 50/51] Change name of SpringBootApplication class --- .../{TaskSchedulerApplication.java => TaskApplication.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/main/java/com/javanauta/ts/taskscheduler/{TaskSchedulerApplication.java => TaskApplication.java} (83%) diff --git a/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java b/src/main/java/com/javanauta/ts/taskscheduler/TaskApplication.java similarity index 83% rename from src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java rename to src/main/java/com/javanauta/ts/taskscheduler/TaskApplication.java index 3321a2b..994f6cb 100644 --- a/src/main/java/com/javanauta/ts/taskscheduler/TaskSchedulerApplication.java +++ b/src/main/java/com/javanauta/ts/taskscheduler/TaskApplication.java @@ -9,8 +9,8 @@ @SpringBootApplication(exclude = MongoAutoConfiguration.class) @EnableScheduling @EnableRabbit -public class TaskSchedulerApplication { +public class TaskApplication { public static void main(String[] args) { - SpringApplication.run(TaskSchedulerApplication.class, args); + SpringApplication.run(TaskApplication.class, args); } } From 0c9e800ab2eb288a33f744323f72490243af9deb Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Fri, 7 Aug 2026 12:26:08 +0100 Subject: [PATCH 51/51] Introduce docker-publish GH workflow to build Docker image, publish to GHCR and trigger ts-system deployment workflow + update existing one (gradle -> pr_check + improvements) --- .github/workflows/docker-publish.yml | 81 ++++++++++++++++++++++++++++ .github/workflows/gradle.yml | 41 -------------- .github/workflows/pr-check.yml | 34 ++++++++++++ 3 files changed, 115 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/docker-publish.yml delete mode 100644 .github/workflows/gradle.yml create mode 100644 .github/workflows/pr-check.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..4ab8b8e --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,81 @@ +name: Build and Publish Docker Image + +on: + push: + branches: + - master + +permissions: + contents: read + packages: write + +jobs: + build-publish: + runs-on: ubuntu-latest + + outputs: + image_tag: ${{ steps.meta.outputs.sha_tag }} + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Java 17 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + cache: gradle + + - name: Grant execute permission for Gradle + run: chmod +x gradlew + + - name: Run tests + run: ./gradlew test + + - name: Set image metadata + id: meta + run: | + SHORT_SHA=${GITHUB_SHA::7} + echo "sha_tag=sha-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and push multi-arch image + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/enzobbom/ts-task:master + ghcr.io/enzobbom/ts-task:${{ steps.meta.outputs.sha_tag }} + labels: | + org.opencontainers.image.source=https://github.com/enzobbom/ts-task + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Trigger ts-system + env: + GH_TOKEN: ${{ secrets.TS_SYSTEM_PAT }} + IMAGE_TAG: ${{ steps.meta.outputs.sha_tag }} + run: | + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + https://api.github.com/repos/enzobbom/ts-system/dispatches \ + -d "$(jq -n \ + --arg service "task" \ + --arg tag "${IMAGE_TAG}" \ + '{event_type:"update-image",client_payload:{service:$service,tag:$tag}}')" \ No newline at end of file diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index 61eae46..0000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: PR Workflow - -on: - pull_request: - branches: - - master - types: - - opened - - synchronize - - reopened - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - - name: Cache Gradle packages - uses: actions/cache@v3 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build with Gradle - run: ./gradlew build - - - name: Run tests - run: ./gradlew test diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..129794a --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,34 @@ +name: PR Check + +on: + pull_request: + branches: + - master + types: + - opened + - synchronize + - reopened + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build \ No newline at end of file