From 205a080993ed929443cca7bebbba37c6bf122877 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Fri, 21 Mar 2025 14:45:56 +0500 Subject: [PATCH 01/17] add migration file for task_skills table --- .../migrations/V2__task_skills_association_table.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 skill-tree/src/main/resources/db/migrations/V2__task_skills_association_table.sql diff --git a/skill-tree/src/main/resources/db/migrations/V2__task_skills_association_table.sql b/skill-tree/src/main/resources/db/migrations/V2__task_skills_association_table.sql new file mode 100644 index 00000000..527e3734 --- /dev/null +++ b/skill-tree/src/main/resources/db/migrations/V2__task_skills_association_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE `task_skills` ( + `task_id` varchar(255) NOT NULL, + `skill_id` int NOT NULL, + `created_at` datetime(6) NOT NULL, + `is_deleted` tinyint(1) NOT NULL DEFAULT '0', + `updated_at` datetime(6) DEFAULT NULL, + `created_by` varchar(255) NOT NULL, + `updated_by` varchar(255) DEFAULT NULL, + PRIMARY KEY (`task_id`, `skill_id`), + CONSTRAINT `fk_task_skills_skill_id` FOREIGN KEY (`skill_id`) REFERENCES `skills` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; \ No newline at end of file From 0a0d814f2dea3a779435c630d9bc4563490c493a Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Fri, 21 Mar 2025 16:07:15 +0500 Subject: [PATCH 02/17] add TaskSkillId as composite key and TaskSkill entity --- .../com/RDS/skilltree/models/TaskSkill.java | 33 +++++++++++++++++++ .../com/RDS/skilltree/models/TaskSkillId.java | 17 ++++++++++ 2 files changed, 50 insertions(+) create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java b/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java new file mode 100644 index 00000000..dce2ce0b --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java @@ -0,0 +1,33 @@ +package com.RDS.skilltree.models; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import lombok.*; + +@Entity +@Table(name = "task_skills") +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TaskSkill { + + @EmbeddedId private TaskSkillId id; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "is_deleted", nullable = false) + @Builder.Default + private boolean isDeleted = false; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "created_by", nullable = false) + private String createdBy; + + @Column(name = "updated_by") + private String updatedBy; +} diff --git a/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java b/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java new file mode 100644 index 00000000..7329f37d --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java @@ -0,0 +1,17 @@ +package com.RDS.skilltree.models; + +import jakarta.persistence.Embeddable; +import java.io.Serializable; +import lombok.*; + +@Embeddable +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode +public class TaskSkillId implements Serializable { + private String taskId; + private Integer skillId; +} From ac4a65367adea0597560c6a76c81316c14ce9847 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Fri, 21 Mar 2025 16:12:08 +0500 Subject: [PATCH 03/17] add TaskSkillRepository to find skills by taskId --- .../skilltree/repositories/TaskSkillRepository.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java new file mode 100644 index 00000000..7094a5b3 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java @@ -0,0 +1,12 @@ +package com.RDS.skilltree.repositories; + +import com.RDS.skilltree.models.TaskSkill; +import com.RDS.skilltree.models.TaskSkillId; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TaskSkillRepository extends JpaRepository { + List findByIdTaskId(String taskId); +} From 819284e07375c6020ff16cc8b78bf90d506b30a7 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Fri, 21 Mar 2025 16:16:39 +0500 Subject: [PATCH 04/17] add custom exception for duplicate task-skill association --- .../TaskSkillAssociationAlreadyExistsException.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/exceptions/TaskSkillAssociationAlreadyExistsException.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/exceptions/TaskSkillAssociationAlreadyExistsException.java b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/TaskSkillAssociationAlreadyExistsException.java new file mode 100644 index 00000000..661d2b2f --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/TaskSkillAssociationAlreadyExistsException.java @@ -0,0 +1,11 @@ +package com.RDS.skilltree.exceptions; + +public class TaskSkillAssociationAlreadyExistsException extends RuntimeException { + public TaskSkillAssociationAlreadyExistsException(String message, Throwable cause) { + super(message, cause); + } + + public TaskSkillAssociationAlreadyExistsException(String message) { + super(message); + } +} From 0f8f19307873403f1bcdb96705e8dcc018d7065e Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Fri, 21 Mar 2025 18:24:03 +0500 Subject: [PATCH 05/17] add TaskSkillService to handle services related to task and skill --- .../skilltree/services/TaskSkillService.java | 7 +++ .../TaskSkillServiceImplementation.java | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java new file mode 100644 index 00000000..2a69c567 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java @@ -0,0 +1,7 @@ +package com.RDS.skilltree.services; + +import java.util.List; + +public interface TaskSkillService { + void createTaskSkills(String taskId, List skillIds, String createdBy); +} diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java new file mode 100644 index 00000000..be1f79fb --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java @@ -0,0 +1,48 @@ +package com.RDS.skilltree.services; + +import com.RDS.skilltree.exceptions.SkillNotFoundException; +import com.RDS.skilltree.exceptions.TaskSkillAssociationAlreadyExistsException; +import com.RDS.skilltree.models.TaskSkill; +import com.RDS.skilltree.models.TaskSkillId; +import com.RDS.skilltree.repositories.SkillRepository; +import com.RDS.skilltree.repositories.TaskSkillRepository; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class TaskSkillServiceImplementation implements TaskSkillService { + + private final TaskSkillRepository taskSkillRepository; + private final SkillRepository skillRepository; + + public TaskSkillServiceImplementation( + TaskSkillRepository taskSkillRepository, SkillRepository skillRepository) { + this.taskSkillRepository = taskSkillRepository; + this.skillRepository = skillRepository; + } + + @Override + @Transactional + public void createTaskSkills(String taskId, List skillIds, String createdBy) { + LocalDateTime now = LocalDateTime.now(); + for (Integer skillId : skillIds) { + // Check if the skill exists; if not, throw SkillNotFoundException. + if (!skillRepository.existsById(skillId)) { + throw new SkillNotFoundException("Skill not found for skillId = " + skillId); + } + // Create a composite key for the association. + TaskSkillId tsId = new TaskSkillId(taskId, skillId); + // Explicitly check if an association already exists. + if (taskSkillRepository.existsById(tsId)) { + throw new TaskSkillAssociationAlreadyExistsException( + "Task-Skill association already exists for task " + taskId + " and skill " + skillId); + } + // Create and save the new association. + TaskSkill taskSkill = + TaskSkill.builder().id(tsId).createdAt(now).createdBy(createdBy).build(); + taskSkillRepository.saveAndFlush(taskSkill); + } + } +} From fe7ead34278622c76f81f09d45fa3ec7cd514ac8 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Fri, 21 Mar 2025 18:33:05 +0500 Subject: [PATCH 06/17] add controller TaskSkillApi --- .../com/RDS/skilltree/apis/TaskSkillApi.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java b/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java new file mode 100644 index 00000000..70bc46dc --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java @@ -0,0 +1,52 @@ +package com.RDS.skilltree.apis; + +import com.RDS.skilltree.annotations.AuthorizedRoles; +import com.RDS.skilltree.enums.UserRoleEnum; +import com.RDS.skilltree.exceptions.TaskSkillAssociationAlreadyExistsException; +import com.RDS.skilltree.models.JwtUser; +import com.RDS.skilltree.services.TaskSkillService; +import java.util.List; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("v1/tasks") +public class TaskSkillApi { + + private final TaskSkillService taskSkillService; + + @AuthorizedRoles({UserRoleEnum.SUPERUSER}) + @PostMapping("/{taskId}/skills") + public ResponseEntity createTaskSkills( + @PathVariable String taskId, @RequestBody TaskSkillsRequest request) { + // Extract the authenticated user's details from the security context. + JwtUser currentUser = + (JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + String createdBy = currentUser.getRdsUserId(); // Save the user's unique identifier + taskSkillService.createTaskSkills(taskId, request.getSkillIds(), createdBy); + return ResponseEntity.ok(new ApiResponse("Skills are linked to task successfully!")); + } + + @ExceptionHandler(TaskSkillAssociationAlreadyExistsException.class) + public ResponseEntity handleTaskSkillAssociationAlreadyExistsException( + TaskSkillAssociationAlreadyExistsException ex) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new ApiResponse(ex.getMessage())); + } + + @Data + public static class TaskSkillsRequest { + private List skillIds; + } + + @Data + public static class ApiResponse { + private final String message; + } +} From 79c9765e143450282565c79d886ec1c14112df25 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Mon, 24 Mar 2025 19:29:20 +0500 Subject: [PATCH 07/17] add @NotNull validation and serialVersionUID to TaskSkillID model --- .../main/java/com/RDS/skilltree/models/TaskSkillId.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java b/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java index 7329f37d..1f850152 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java @@ -1,6 +1,8 @@ package com.RDS.skilltree.models; import jakarta.persistence.Embeddable; +import jakarta.validation.constraints.NotNull; +import java.io.Serial; import java.io.Serializable; import lombok.*; @@ -12,6 +14,11 @@ @Builder @EqualsAndHashCode public class TaskSkillId implements Serializable { + @Serial private static final long serialVersionUID = 1L; + + @NotNull(message = "Task ID cannot be null") private String taskId; + + @NotNull(message = "Skill ID cannot be null") private Integer skillId; } From e2c5248d3261add5c52ea7f4631b391e5942ee1d Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 27 Mar 2025 19:38:19 +0500 Subject: [PATCH 08/17] fix: remove duplicate skill IDs sent through request body --- .../skilltree/services/TaskSkillServiceImplementation.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java index be1f79fb..db495124 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java @@ -7,7 +7,9 @@ import com.RDS.skilltree.repositories.SkillRepository; import com.RDS.skilltree.repositories.TaskSkillRepository; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,7 +29,9 @@ public TaskSkillServiceImplementation( @Transactional public void createTaskSkills(String taskId, List skillIds, String createdBy) { LocalDateTime now = LocalDateTime.now(); - for (Integer skillId : skillIds) { + // Remove duplicate skill IDs + Set uniqueSkillIds = new HashSet<>(skillIds); + for (Integer skillId : uniqueSkillIds) { // Check if the skill exists; if not, throw SkillNotFoundException. if (!skillRepository.existsById(skillId)) { throw new SkillNotFoundException("Skill not found for skillId = " + skillId); From 587069aec111c7775176f687e0b93f711a64d5eb Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 27 Mar 2025 19:56:57 +0500 Subject: [PATCH 09/17] add relationship mapping in TaskSkill and ensure Skill is fetched before saving --- .../com/RDS/skilltree/models/TaskSkill.java | 18 ++++++++++++++++-- .../TaskSkillServiceImplementation.java | 14 +++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java b/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java index dce2ce0b..3b925dfa 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java @@ -3,6 +3,10 @@ import jakarta.persistence.*; import java.time.LocalDateTime; import lombok.*; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; @Entity @Table(name = "task_skills") @@ -15,19 +19,29 @@ public class TaskSkill { @EmbeddedId private TaskSkillId id; - @Column(name = "created_at", nullable = false) + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @Column(name = "is_deleted", nullable = false) @Builder.Default private boolean isDeleted = false; + @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; - @Column(name = "created_by", nullable = false) + @CreatedBy + @Column(name = "created_by", nullable = false, updatable = false) private String createdBy; + @LastModifiedBy @Column(name = "updated_by") private String updatedBy; + + // Relationship to Skill entity for richer detail access. + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("skillId") + @JoinColumn(name = "skill_id", insertable = false, updatable = false) + private Skill skill; } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java index db495124..0b9d1ef9 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java @@ -2,6 +2,7 @@ import com.RDS.skilltree.exceptions.SkillNotFoundException; import com.RDS.skilltree.exceptions.TaskSkillAssociationAlreadyExistsException; +import com.RDS.skilltree.models.Skill; import com.RDS.skilltree.models.TaskSkill; import com.RDS.skilltree.models.TaskSkillId; import com.RDS.skilltree.repositories.SkillRepository; @@ -32,10 +33,13 @@ public void createTaskSkills(String taskId, List skillIds, String creat // Remove duplicate skill IDs Set uniqueSkillIds = new HashSet<>(skillIds); for (Integer skillId : uniqueSkillIds) { - // Check if the skill exists; if not, throw SkillNotFoundException. - if (!skillRepository.existsById(skillId)) { - throw new SkillNotFoundException("Skill not found for skillId = " + skillId); - } + // Fetch the skill entity before using it in TaskSkill. + Skill skill = + skillRepository + .findById(skillId) + .orElseThrow( + () -> new SkillNotFoundException("Skill not found for skillId = " + skillId)); + // Create a composite key for the association. TaskSkillId tsId = new TaskSkillId(taskId, skillId); // Explicitly check if an association already exists. @@ -45,7 +49,7 @@ public void createTaskSkills(String taskId, List skillIds, String creat } // Create and save the new association. TaskSkill taskSkill = - TaskSkill.builder().id(tsId).createdAt(now).createdBy(createdBy).build(); + TaskSkill.builder().id(tsId).skill(skill).createdAt(now).createdBy(createdBy).build(); taskSkillRepository.saveAndFlush(taskSkill); } } From 389c38be94dbf5d42cb891d64836fad879d2cfc8 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 27 Mar 2025 20:08:02 +0500 Subject: [PATCH 10/17] create TaskSkill request and creation view models --- .../viewmodels/CreateTaskSkillViewModel.java | 12 ++++++++++++ .../viewmodels/TaskSkillRequestViewModel.java | 15 +++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/viewmodels/CreateTaskSkillViewModel.java create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/viewmodels/TaskSkillRequestViewModel.java diff --git a/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/CreateTaskSkillViewModel.java b/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/CreateTaskSkillViewModel.java new file mode 100644 index 00000000..ce762c19 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/CreateTaskSkillViewModel.java @@ -0,0 +1,12 @@ +package com.RDS.skilltree.viewmodels; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreateTaskSkillViewModel { + private String message; +} diff --git a/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/TaskSkillRequestViewModel.java b/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/TaskSkillRequestViewModel.java new file mode 100644 index 00000000..8673d794 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/viewmodels/TaskSkillRequestViewModel.java @@ -0,0 +1,15 @@ +package com.RDS.skilltree.viewmodels; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TaskSkillRequestViewModel { + @NotEmpty(message = "Skill IDs list cannot be empty") + private List skillIds; +} From f2819d28b26b5c185932b72f2f3fecc439df9fde Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 27 Mar 2025 20:22:14 +0500 Subject: [PATCH 11/17] use dedicated TaskSkill request and creation view-models for modularity --- .../com/RDS/skilltree/apis/TaskSkillApi.java | 32 +++++++------------ .../skilltree/services/TaskSkillService.java | 4 ++- .../TaskSkillServiceImplementation.java | 5 ++- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java b/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java index 70bc46dc..b9304ce7 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java @@ -5,8 +5,9 @@ import com.RDS.skilltree.exceptions.TaskSkillAssociationAlreadyExistsException; import com.RDS.skilltree.models.JwtUser; import com.RDS.skilltree.services.TaskSkillService; -import java.util.List; -import lombok.Data; +import com.RDS.skilltree.viewmodels.CreateTaskSkillViewModel; +import com.RDS.skilltree.viewmodels.TaskSkillRequestViewModel; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -24,29 +25,20 @@ public class TaskSkillApi { @AuthorizedRoles({UserRoleEnum.SUPERUSER}) @PostMapping("/{taskId}/skills") - public ResponseEntity createTaskSkills( - @PathVariable String taskId, @RequestBody TaskSkillsRequest request) { - // Extract the authenticated user's details from the security context. + public ResponseEntity createTaskSkills( + @PathVariable String taskId, @Valid @RequestBody TaskSkillRequestViewModel request) { JwtUser currentUser = (JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - String createdBy = currentUser.getRdsUserId(); // Save the user's unique identifier - taskSkillService.createTaskSkills(taskId, request.getSkillIds(), createdBy); - return ResponseEntity.ok(new ApiResponse("Skills are linked to task successfully!")); + String createdBy = currentUser.getRdsUserId(); + CreateTaskSkillViewModel response = + taskSkillService.createTaskSkills(taskId, request.getSkillIds(), createdBy); + return ResponseEntity.ok(response); } @ExceptionHandler(TaskSkillAssociationAlreadyExistsException.class) - public ResponseEntity handleTaskSkillAssociationAlreadyExistsException( + public ResponseEntity handleDuplicateAssociation( TaskSkillAssociationAlreadyExistsException ex) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(new ApiResponse(ex.getMessage())); - } - - @Data - public static class TaskSkillsRequest { - private List skillIds; - } - - @Data - public static class ApiResponse { - private final String message; + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(new CreateTaskSkillViewModel(ex.getMessage())); } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java index 2a69c567..5328e91f 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java @@ -1,7 +1,9 @@ package com.RDS.skilltree.services; +import com.RDS.skilltree.viewmodels.CreateTaskSkillViewModel; import java.util.List; public interface TaskSkillService { - void createTaskSkills(String taskId, List skillIds, String createdBy); + CreateTaskSkillViewModel createTaskSkills( + String taskId, List skillIds, String createdBy); } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java index 0b9d1ef9..385485d4 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java @@ -7,6 +7,7 @@ import com.RDS.skilltree.models.TaskSkillId; import com.RDS.skilltree.repositories.SkillRepository; import com.RDS.skilltree.repositories.TaskSkillRepository; +import com.RDS.skilltree.viewmodels.CreateTaskSkillViewModel; import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; @@ -28,7 +29,8 @@ public TaskSkillServiceImplementation( @Override @Transactional - public void createTaskSkills(String taskId, List skillIds, String createdBy) { + public CreateTaskSkillViewModel createTaskSkills( + String taskId, List skillIds, String createdBy) { LocalDateTime now = LocalDateTime.now(); // Remove duplicate skill IDs Set uniqueSkillIds = new HashSet<>(skillIds); @@ -52,5 +54,6 @@ public void createTaskSkills(String taskId, List skillIds, String creat TaskSkill.builder().id(tsId).skill(skill).createdAt(now).createdBy(createdBy).build(); taskSkillRepository.saveAndFlush(taskSkill); } + return new CreateTaskSkillViewModel("Skills are linked to task successfully!"); } } From be90dd46ee60931f8100bf46fdcaac8087d6d02e Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 27 Mar 2025 22:42:27 +0500 Subject: [PATCH 12/17] add validation for taskId and move exception handler from TaskSkillApi to GlobalExceptionHandler --- .../com/RDS/skilltree/apis/TaskSkillApi.java | 16 ++++++---------- .../exceptions/GlobalExceptionHandler.java | 8 ++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java b/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java index b9304ce7..337c528c 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java @@ -2,23 +2,24 @@ import com.RDS.skilltree.annotations.AuthorizedRoles; import com.RDS.skilltree.enums.UserRoleEnum; -import com.RDS.skilltree.exceptions.TaskSkillAssociationAlreadyExistsException; import com.RDS.skilltree.models.JwtUser; import com.RDS.skilltree.services.TaskSkillService; import com.RDS.skilltree.viewmodels.CreateTaskSkillViewModel; import com.RDS.skilltree.viewmodels.TaskSkillRequestViewModel; import jakarta.validation.Valid; +import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @Slf4j @RestController @RequiredArgsConstructor @RequestMapping("v1/tasks") +@Validated public class TaskSkillApi { private final TaskSkillService taskSkillService; @@ -26,7 +27,9 @@ public class TaskSkillApi { @AuthorizedRoles({UserRoleEnum.SUPERUSER}) @PostMapping("/{taskId}/skills") public ResponseEntity createTaskSkills( - @PathVariable String taskId, @Valid @RequestBody TaskSkillRequestViewModel request) { + @PathVariable @Pattern(regexp = "^[A-Za-z0-9]+$", message = "Task ID must be valid") + String taskId, + @Valid @RequestBody TaskSkillRequestViewModel request) { JwtUser currentUser = (JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String createdBy = currentUser.getRdsUserId(); @@ -34,11 +37,4 @@ public ResponseEntity createTaskSkills( taskSkillService.createTaskSkills(taskId, request.getSkillIds(), createdBy); return ResponseEntity.ok(response); } - - @ExceptionHandler(TaskSkillAssociationAlreadyExistsException.class) - public ResponseEntity handleDuplicateAssociation( - TaskSkillAssociationAlreadyExistsException ex) { - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(new CreateTaskSkillViewModel(ex.getMessage())); - } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java index e5abe97c..40541670 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/exceptions/GlobalExceptionHandler.java @@ -126,6 +126,14 @@ public ResponseEntity handleSkillNotFoundException( return new ResponseEntity<>(new GenericResponse<>(null, ex.getMessage()), HttpStatus.NOT_FOUND); } + @ExceptionHandler(TaskSkillAssociationAlreadyExistsException.class) + public ResponseEntity> handleTaskSkillAssociationAlreadyExistsException( + TaskSkillAssociationAlreadyExistsException ex) { + log.error("Duplicate task-skill association: {}", ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(new GenericResponse<>(null, ex.getMessage())); + } + @ExceptionHandler(EndorsementNotFoundException.class) public ResponseEntity handleEndorsementNotException( EndorsementNotFoundException ex, WebRequest request) { From 7b56aba6e6e12029441d20beee28a0f0eb164a8b Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Thu, 27 Mar 2025 22:50:43 +0500 Subject: [PATCH 13/17] add detailed JavaDoc to TaskSkillService#createTaskSkills --- .../com/RDS/skilltree/services/TaskSkillService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java index 5328e91f..06984232 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillService.java @@ -1,9 +1,21 @@ package com.RDS.skilltree.services; +import com.RDS.skilltree.exceptions.SkillNotFoundException; +import com.RDS.skilltree.exceptions.TaskSkillAssociationAlreadyExistsException; import com.RDS.skilltree.viewmodels.CreateTaskSkillViewModel; import java.util.List; public interface TaskSkillService { + /** + * Creates associations between a task and multiple skills. + * + * @param taskId The unique identifier of the task. + * @param skillIds List of skill identifiers to associate with the task. + * @param createdBy The identifier of the user creating these associations. + * @return A response view model indicating success. + * @throws TaskSkillAssociationAlreadyExistsException if an association already exists. + * @throws SkillNotFoundException if any skill does not exist. + */ CreateTaskSkillViewModel createTaskSkills( String taskId, List skillIds, String createdBy); } From 4b550660c62b85b2b7a84cdbb91d35c9ca30ec29 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Fri, 28 Mar 2025 11:58:21 +0500 Subject: [PATCH 14/17] optimize TaskSkillService by batch retrieving Skill entities for createTaskSkills --- .../repositories/TaskSkillRepository.java | 2 +- .../TaskSkillServiceImplementation.java | 37 +++++++++++++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java index 7094a5b3..bb871945 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java @@ -8,5 +8,5 @@ @Repository public interface TaskSkillRepository extends JpaRepository { - List findByIdTaskId(String taskId); + List findAllByIdTaskId(String taskId); } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java index 385485d4..d69d80ad 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java @@ -12,6 +12,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,24 +35,38 @@ public CreateTaskSkillViewModel createTaskSkills( LocalDateTime now = LocalDateTime.now(); // Remove duplicate skill IDs Set uniqueSkillIds = new HashSet<>(skillIds); - for (Integer skillId : uniqueSkillIds) { - // Fetch the skill entity before using it in TaskSkill. - Skill skill = - skillRepository - .findById(skillId) - .orElseThrow( - () -> new SkillNotFoundException("Skill not found for skillId = " + skillId)); - // Create a composite key for the association. + List skills = skillRepository.findAllById(uniqueSkillIds); + // Verify if any requested skill ID is missing + if (skills.size() != uniqueSkillIds.size()) { + // Determine missing IDs + Set foundIds = skills.stream().map(Skill::getId).collect(Collectors.toSet()); + uniqueSkillIds.removeAll(foundIds); + throw new SkillNotFoundException("Skill not found for skillId(s): " + uniqueSkillIds); + } + for (Integer skillId : uniqueSkillIds) { TaskSkillId tsId = new TaskSkillId(taskId, skillId); - // Explicitly check if an association already exists. + if (taskSkillRepository.existsById(tsId)) { throw new TaskSkillAssociationAlreadyExistsException( "Task-Skill association already exists for task " + taskId + " and skill " + skillId); } - // Create and save the new association. + + // Find the corresponding Skill object from the fetched list + Skill skill = + skills.stream() + .filter(s -> s.getId().equals(skillId)) + .findFirst() + .orElseThrow( + () -> new SkillNotFoundException("Skill not found for skillId = " + skillId)); + TaskSkill taskSkill = - TaskSkill.builder().id(tsId).skill(skill).createdAt(now).createdBy(createdBy).build(); + TaskSkill.builder() + .id(tsId) + .skill(skill) // Set the Skill relationship + .createdAt(now) + .createdBy(createdBy) + .build(); taskSkillRepository.saveAndFlush(taskSkill); } return new CreateTaskSkillViewModel("Skills are linked to task successfully!"); From 6f0227aa4d047008e4f3be04f3b58e2d11a1112d Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Fri, 28 Mar 2025 19:50:05 +0500 Subject: [PATCH 15/17] optimize TaskSkillService by batch validating and saving TaskSkill associations --- .../TaskSkillServiceImplementation.java | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java index d69d80ad..415ef1ab 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java @@ -44,31 +44,42 @@ public CreateTaskSkillViewModel createTaskSkills( uniqueSkillIds.removeAll(foundIds); throw new SkillNotFoundException("Skill not found for skillId(s): " + uniqueSkillIds); } - for (Integer skillId : uniqueSkillIds) { - TaskSkillId tsId = new TaskSkillId(taskId, skillId); + List taskSkillsToSave = + uniqueSkillIds.stream() + .map( + skillId -> { + TaskSkillId tsId = new TaskSkillId(taskId, skillId); - if (taskSkillRepository.existsById(tsId)) { - throw new TaskSkillAssociationAlreadyExistsException( - "Task-Skill association already exists for task " + taskId + " and skill " + skillId); - } + if (taskSkillRepository.existsById(tsId)) { + throw new TaskSkillAssociationAlreadyExistsException( + "Task-Skill association already exists for task " + + taskId + + " and skill " + + skillId); + } - // Find the corresponding Skill object from the fetched list - Skill skill = - skills.stream() - .filter(s -> s.getId().equals(skillId)) - .findFirst() - .orElseThrow( - () -> new SkillNotFoundException("Skill not found for skillId = " + skillId)); + // Find the corresponding Skill object from the fetched list + Skill skill = + skills.stream() + .filter(s -> s.getId().equals(skillId)) + .findFirst() + .orElseThrow( + () -> + new SkillNotFoundException( + "Skill not found for skillId = " + skillId)); + + return TaskSkill.builder() + .id(tsId) + .skill(skill) // Set the Skill relationship + .createdAt(now) + .createdBy(createdBy) + .build(); + }) + .collect(Collectors.toList()); + + // Save all TaskSkill entities in one batch call + taskSkillRepository.saveAll(taskSkillsToSave); - TaskSkill taskSkill = - TaskSkill.builder() - .id(tsId) - .skill(skill) // Set the Skill relationship - .createdAt(now) - .createdBy(createdBy) - .build(); - taskSkillRepository.saveAndFlush(taskSkill); - } return new CreateTaskSkillViewModel("Skills are linked to task successfully!"); } } From 9c76736530f35c157ddc2abba1046c5feae7b9bd Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Sat, 29 Mar 2025 14:15:23 +0500 Subject: [PATCH 16/17] update status code 200 to 201 for resource creation --- .../src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java b/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java index 337c528c..7aa5120d 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java @@ -10,6 +10,7 @@ import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.validation.annotation.Validated; @@ -35,6 +36,6 @@ public ResponseEntity createTaskSkills( String createdBy = currentUser.getRdsUserId(); CreateTaskSkillViewModel response = taskSkillService.createTaskSkills(taskId, request.getSkillIds(), createdBy); - return ResponseEntity.ok(response); + return ResponseEntity.status(HttpStatus.CREATED).body(response); } } From 92df1eac17a53b7a6e2a21f6a3ad23b483dac312 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Date: Wed, 9 Apr 2025 10:05:07 +0500 Subject: [PATCH 17/17] optimize task-skill creation with batch checks and map-based lookups --- .../repositories/TaskSkillRepository.java | 12 +++- .../TaskSkillServiceImplementation.java | 67 +++++++++---------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java index bb871945..1e724d60 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/repositories/TaskSkillRepository.java @@ -2,11 +2,19 @@ import com.RDS.skilltree.models.TaskSkill; import com.RDS.skilltree.models.TaskSkillId; -import java.util.List; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface TaskSkillRepository extends JpaRepository { - List findAllByIdTaskId(String taskId); + /** + * Find skill IDs that already have associations with the given task ID + */ + @Query( + "SELECT ts.id.skillId FROM TaskSkill ts WHERE ts.id.taskId = :taskId AND ts.id.skillId IN :skillIds") + Set findSkillIdsByTaskIdAndSkillIdIn( + @Param("taskId") String taskId, @Param("skillIds") Set skillIds); } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java index 415ef1ab..092a24b9 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/services/TaskSkillServiceImplementation.java @@ -11,6 +11,7 @@ import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -32,52 +33,48 @@ public TaskSkillServiceImplementation( @Transactional public CreateTaskSkillViewModel createTaskSkills( String taskId, List skillIds, String createdBy) { - LocalDateTime now = LocalDateTime.now(); // Remove duplicate skill IDs Set uniqueSkillIds = new HashSet<>(skillIds); + // FIRST: Check if any TaskSkill associations already exist (batch check) + Set existingSkillIds = + taskSkillRepository.findSkillIdsByTaskIdAndSkillIdIn(taskId, uniqueSkillIds); + if (!existingSkillIds.isEmpty()) { + throw new TaskSkillAssociationAlreadyExistsException( + "Task-Skill associations already exist for task " + + taskId + + " and skills: " + + existingSkillIds); + } + + // SECOND: Load all skills at once and verify they exist List skills = skillRepository.findAllById(uniqueSkillIds); - // Verify if any requested skill ID is missing + + // Check for missing skills if (skills.size() != uniqueSkillIds.size()) { - // Determine missing IDs - Set foundIds = skills.stream().map(Skill::getId).collect(Collectors.toSet()); - uniqueSkillIds.removeAll(foundIds); - throw new SkillNotFoundException("Skill not found for skillId(s): " + uniqueSkillIds); + Set foundSkillIds = skills.stream().map(Skill::getId).collect(Collectors.toSet()); + Set missingIds = new HashSet<>(uniqueSkillIds); + missingIds.removeAll(foundSkillIds); + throw new SkillNotFoundException("Skill not found for skillId(s): " + missingIds); } + + // Create a map for quick lookups + Map skillsMap = skills.stream().collect(Collectors.toMap(Skill::getId, s -> s)); + + // Create and save TaskSkill entities + LocalDateTime now = LocalDateTime.now(); List taskSkillsToSave = uniqueSkillIds.stream() .map( - skillId -> { - TaskSkillId tsId = new TaskSkillId(taskId, skillId); - - if (taskSkillRepository.existsById(tsId)) { - throw new TaskSkillAssociationAlreadyExistsException( - "Task-Skill association already exists for task " - + taskId - + " and skill " - + skillId); - } - - // Find the corresponding Skill object from the fetched list - Skill skill = - skills.stream() - .filter(s -> s.getId().equals(skillId)) - .findFirst() - .orElseThrow( - () -> - new SkillNotFoundException( - "Skill not found for skillId = " + skillId)); - - return TaskSkill.builder() - .id(tsId) - .skill(skill) // Set the Skill relationship - .createdAt(now) - .createdBy(createdBy) - .build(); - }) + skillId -> + TaskSkill.builder() + .id(new TaskSkillId(taskId, skillId)) + .skill(skillsMap.get(skillId)) // Set the actual Skill entity reference + .createdAt(now) + .createdBy(createdBy) + .build()) .collect(Collectors.toList()); - // Save all TaskSkill entities in one batch call taskSkillRepository.saveAll(taskSkillsToSave); return new CreateTaskSkillViewModel("Skills are linked to task successfully!");