diff --git a/pom.xml b/pom.xml index 16784ec..ea91cf3 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,12 @@ spring-boot-starter-oauth2-resource-server + + + org.springframework.kafka + spring-kafka + + org.springframework.boot @@ -106,6 +112,11 @@ org.springframework.security spring-security-test + + org.springframework.kafka + spring-kafka-test + test + diff --git a/src/main/java/com/seyed/ali/timeentryservice/event/ProjectEventListener.java b/src/main/java/com/seyed/ali/timeentryservice/event/ProjectEventListener.java new file mode 100644 index 0000000..fe6f6c9 --- /dev/null +++ b/src/main/java/com/seyed/ali/timeentryservice/event/ProjectEventListener.java @@ -0,0 +1,33 @@ +package com.seyed.ali.timeentryservice.event; + +import com.seyed.ali.timeentryservice.model.enums.OperationType; +import com.seyed.ali.timeentryservice.model.payload.ProjectDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ProjectEventListener { + + private final ProjectEventService projectEventService; + + @KafkaListener( + topics = "${spring.kafka.topic.name}", + groupId = "${spring.kafka.consumer.group-id}" // if we change the group-id name, the events from the previous data will also be logged, otherwise, only the new events will be logged! + ) + public void handleProjectEvent(@Payload ConsumerRecord record, @Header("OperationType") String operationType) { + ProjectDTO projectDTO = record.value(); + switch (OperationType.valueOf(operationType)) { + case DELETE -> this.projectEventService.handleDeleteOperation(projectDTO); + case DETACH -> this.projectEventService.handleDetachOperation(projectDTO); + default -> log.warn("\"{}\" operation type not supported.", operationType); + } + } + +} diff --git a/src/main/java/com/seyed/ali/timeentryservice/event/ProjectEventService.java b/src/main/java/com/seyed/ali/timeentryservice/event/ProjectEventService.java new file mode 100644 index 0000000..2a4dfed --- /dev/null +++ b/src/main/java/com/seyed/ali/timeentryservice/event/ProjectEventService.java @@ -0,0 +1,36 @@ +package com.seyed.ali.timeentryservice.event; + +import com.seyed.ali.timeentryservice.model.payload.ProjectDTO; +import com.seyed.ali.timeentryservice.repository.TimeEntryRepository; +import com.seyed.ali.timeentryservice.service.interfaces.TimeEntryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ProjectEventService { + + private final TimeEntryService timeEntryService; + private final TimeEntryRepository timeEntryRepository; + + public void handleDeleteOperation(ProjectDTO projectDTO) { + log.info("\"Delete\" project, associated tasks & time entries: {}", projectDTO); + this.timeEntryRepository.findByProjectId(projectDTO.getProjectId()) + .forEach(this.timeEntryService::deleteTimeEntry); + } + + public void handleDetachOperation(ProjectDTO projectDTO) { + log.info("\"Detach\" project, associated tasks & time entries: {}", projectDTO); + this.timeEntryRepository.findByProjectId(projectDTO.getProjectId()) + .forEach(timeEntry -> { + timeEntry.setProjectId(null); + timeEntry.setTaskId(null); + this.timeEntryRepository.save(timeEntry); + }); + } + +} diff --git a/src/main/java/com/seyed/ali/timeentryservice/model/enums/OperationType.java b/src/main/java/com/seyed/ali/timeentryservice/model/enums/OperationType.java new file mode 100644 index 0000000..d402fa4 --- /dev/null +++ b/src/main/java/com/seyed/ali/timeentryservice/model/enums/OperationType.java @@ -0,0 +1,7 @@ +package com.seyed.ali.timeentryservice.model.enums; + +public enum OperationType { + + DELETE, DETACH + +} diff --git a/src/main/java/com/seyed/ali/timeentryservice/model/payload/ProjectDTO.java b/src/main/java/com/seyed/ali/timeentryservice/model/payload/ProjectDTO.java new file mode 100644 index 0000000..0bb96bf --- /dev/null +++ b/src/main/java/com/seyed/ali/timeentryservice/model/payload/ProjectDTO.java @@ -0,0 +1,39 @@ +package com.seyed.ali.timeentryservice.model.payload; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * DTO for {@link com.seyed.ali.projectservice.model.domain.Project} + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ProjectDTO implements Serializable { + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Unique identifier for the project", example = "12345") + private String projectId; + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "The project name", example = "Microservices-Springboot") + private String projectName; + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "The project name", example = "Learning microservices is really exciting and HARD ;)") + private String projectDescription; + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "The task associated with the project", implementation = TaskDTO.class) + private List taskDTO = new ArrayList<>(); + + public ProjectDTO(String projectId, String projectName, String projectDescription) { + this.projectId = projectId; + this.projectName = projectName; + this.projectDescription = projectDescription; + this.taskDTO = new ArrayList<>(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/seyed/ali/timeentryservice/repository/TimeEntryRepository.java b/src/main/java/com/seyed/ali/timeentryservice/repository/TimeEntryRepository.java index 5dac1b1..56e9ed5 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/repository/TimeEntryRepository.java +++ b/src/main/java/com/seyed/ali/timeentryservice/repository/TimeEntryRepository.java @@ -3,6 +3,7 @@ import com.seyed.ali.timeentryservice.model.domain.TimeEntry; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface TimeEntryRepository extends JpaRepository { @@ -11,4 +12,8 @@ public interface TimeEntryRepository extends JpaRepository { TimeEntry findByUserIdAndTimeEntryId(String userId, String timeEntryId); + List findByProjectId(String projectId); + + List findByTaskId(String taskId); + } \ No newline at end of file diff --git a/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryServiceImpl.java b/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryServiceImpl.java index e952dad..f24f63d 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryServiceImpl.java +++ b/src/main/java/com/seyed/ali/timeentryservice/service/TimeEntryServiceImpl.java @@ -113,4 +113,19 @@ public void deleteTimeEntry(String timeEntryId) { this.timeEntryRepository.deleteById(timeEntryId); } + /** + * {@inheritDoc} + */ + @Override + @Transactional + @CacheEvict( + cacheNames = TimeEntryCacheManager.TIME_ENTRY_CACHE, + key = "#timeEntry.timeEntryId" + ) + public void deleteTimeEntry(TimeEntry timeEntry) { + TimeEntry foundTimeEntry = this.timeEntryRepository.findById(timeEntry.getTimeEntryId()) + .orElseThrow(() -> new ResourceNotFoundException("The provided timeEntryId does not exist")); + this.timeEntryRepository.delete(foundTimeEntry); + } + } diff --git a/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryService.java b/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryService.java index e9d2fc7..a654317 100644 --- a/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryService.java +++ b/src/main/java/com/seyed/ali/timeentryservice/service/interfaces/TimeEntryService.java @@ -55,10 +55,17 @@ public interface TimeEntryService { TimeEntry updateTimeEntryManually(String id, TimeEntryDTO timeEntryDTO); /** - * Deletes a time entry. + * Deletes a time entry by ID. * * @param timeEntryId The ID of the time entry to be deleted. */ void deleteTimeEntry(String timeEntryId); + /** + * Deletes a time entry. + * + * @param timeEntry The time entry to be deleted. + */ + void deleteTimeEntry(TimeEntry timeEntry); + } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index fc151f9..f91d153 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -70,4 +70,23 @@ authentication: service: user-persistence-controller: base-url: http://${AUTHENTICATION_SERVICE_HANDLE_USER_BASE_URL:localhost:8081}/keycloak-user - handle-user-url: /handle-user \ No newline at end of file + handle-user-url: /handle-user + +--- # Kafka +spring: + kafka: + consumer: + bootstrap-servers: localhost:9092 + group-id: project_group_timeentry + auto-offset-reset: earliest + + #configure deserialize classes for key & value pair + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + spring.json.type.mapping: ProjectEvent:com.seyed.ali.timeentryservice.model.payload.ProjectDTO + + #custom + topic: + name: project_name \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 9f6ba4a..87d60e7 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -38,4 +38,23 @@ authentication: service: user-persistence-controller: base-url: http://${AUTHENTICATION_SERVICE_HANDLE_USER_BASE_URL:localhost:8081}/keycloak-user - handle-user-url: /handle-user \ No newline at end of file + handle-user-url: /handle-user + +--- # Kafka +spring: + kafka: + consumer: + bootstrap-servers: localhost:9092 + group-id: project_group_timeentry + auto-offset-reset: earliest + + #configure deserialize classes for key & value pair + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + spring.json.type.mapping: ProjectEvent:com.seyed.ali.timeentryservice.model.payload.ProjectDTO + + #custom + topic: + name: project_name \ No newline at end of file