diff --git a/.gitignore b/.gitignore index c82eeba..abbafae 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ target/ application.properties .settings/ .project -.classpath \ No newline at end of file +.classpath +.vscode/ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1efbdd7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# -------- Build stage -------- +FROM maven:3.9.6-eclipse-temurin-21 AS build + +WORKDIR /app + +COPY pom.xml . +RUN mvn -B dependency:go-offline + +COPY src ./src +RUN mvn -B clean package -DskipTests + +# -------- Runtime stage -------- +FROM eclipse-temurin:21-jre + +WORKDIR /app + +COPY --from=build /app/target/*.jar app.jar + +EXPOSE 9010 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/src/main/java/org/zaine/app/controller/CommentsController.java b/src/main/java/org/zaine/app/controller/CommentsController.java new file mode 100644 index 0000000..83e6b7f --- /dev/null +++ b/src/main/java/org/zaine/app/controller/CommentsController.java @@ -0,0 +1,71 @@ +package org.zaine.app.controller; + +import java.lang.System.Logger; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.zaine.app.dto.CreateCommentDTO; +import org.zaine.app.model.Comments; +import org.zaine.app.service.CommentsService; + +@RestController +@RequestMapping("/api/comments") +public class CommentsController { + private static final Logger logger = System.getLogger(CommentsController.class.getName()); + @Autowired + private CommentsService commentsService; + + @GetMapping("/{page_slug}") + public List getAllCommentsBySlug(@PathVariable String page_slug) { + List allComments = commentsService.getAllCommentsBySlug(page_slug); + return allComments; + + } + + @GetMapping("/item/{id}") + public Comments getCommentById(@PathVariable Integer id) { + return commentsService.getCommentById(id); + } + + @GetMapping("") + public List getAllComments() { + return commentsService.getAllComments(); + } + + + @PostMapping( + value = "", + consumes = "application/json" + ) + public void addComment(@RequestBody CreateCommentDTO dto) { + + if (dto.getContent() == null || dto.getContent().trim().isEmpty()) { + logger.log(System.Logger.Level.WARNING, + "Attempted to add empty comment. Operation aborted."); + return; + } + + Comments newComment = new Comments(); + newComment.setPageSlug(dto.getPageSlug()); + newComment.setAuthor(dto.getAuthor()); + newComment.setContent(dto.getContent()); + newComment.setParentId(dto.getParentId()); + newComment.setCreatedAt(java.time.Instant.now()); + + logger.log(System.Logger.Level.INFO, + "Adding comment to pageSlug: " + dto.getPageSlug()); + + commentsService.addComment(newComment); + } + + @GetMapping("/thread/{comment_id}") + public List getCommentThread(@PathVariable Integer comment_id) { + return commentsService.getCommentThread(comment_id); + } +} diff --git a/src/main/java/org/zaine/app/controller/CompetenciesController.java b/src/main/java/org/zaine/app/controller/CompetenciesController.java new file mode 100644 index 0000000..6af277a --- /dev/null +++ b/src/main/java/org/zaine/app/controller/CompetenciesController.java @@ -0,0 +1,49 @@ +package org.zaine.app.controller; + +import java.lang.System.Logger; +import java.util.List; +import java.util.Map; + +import org.zaine.app.dto.CompetenciesDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.zaine.app.model.Competencies; +import org.zaine.app.service.CompetenciesService; + + +@RestController +@RequestMapping("/api/competencies") +public class CompetenciesController { + + private static final Logger logger = System.getLogger(CompetenciesController.class.getName()); + @Autowired + private CompetenciesService competenciesService; + + @GetMapping("/items") + public List getAllCompetencies() { + + return competenciesService.getAllCompetencies(); + } + + @GetMapping("/item/{id}") + public Competencies getCompetencyById(@PathVariable Integer id) { + return competenciesService.getCompetencyById(id); + } + + @PostMapping( + value = "/items/{id}/state", + consumes = "application/json" + ) + public void updateCompetencyState( + @PathVariable Integer id, + @RequestBody CompetenciesDTO request + ) { + competenciesService.updateCompetencyState(id, request.getState()); + } + +} diff --git a/src/main/java/org/zaine/app/controller/HealthController.java b/src/main/java/org/zaine/app/controller/HealthController.java index 02f9bb4..cc34ba8 100644 --- a/src/main/java/org/zaine/app/controller/HealthController.java +++ b/src/main/java/org/zaine/app/controller/HealthController.java @@ -1,4 +1,4 @@ -package org.zaine.app; +package org.zaine.app.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/org/zaine/app/controller/NotesController.java b/src/main/java/org/zaine/app/controller/NotesController.java index ca8a862..d75e2e8 100644 --- a/src/main/java/org/zaine/app/controller/NotesController.java +++ b/src/main/java/org/zaine/app/controller/NotesController.java @@ -7,11 +7,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.zaine.app.dto.CreateNoteDTO; import org.zaine.app.model.Notes; import org.zaine.app.service.NotesService; import java.util.List; @RestController +@RequestMapping("/api") public class NotesController { @Autowired private NotesService notesService; @@ -22,10 +24,19 @@ public class NotesController { return notesService.getAllNotes(); } - @PostMapping("/notes") - public Notes createNote(@RequestBody Notes note) { + @PostMapping( + value = "/notes", + consumes = "application/json" + ) + public Notes createNote(@RequestBody CreateNoteDTO dto) { + + Notes note = new Notes(); + note.setAuthorName(dto.getAuthorName()); + note.setContent(dto.getContent()); + return notesService.createNote(note); } + diff --git a/src/main/java/org/zaine/app/dto/CompetenciesDTO.java b/src/main/java/org/zaine/app/dto/CompetenciesDTO.java new file mode 100644 index 0000000..3b68ebf --- /dev/null +++ b/src/main/java/org/zaine/app/dto/CompetenciesDTO.java @@ -0,0 +1,18 @@ +package org.zaine.app.dto; + +public class CompetenciesDTO { + + private String state; + + public CompetenciesDTO() { + // REQUIRED: default constructor + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } +} diff --git a/src/main/java/org/zaine/app/dto/CreateCommentDTO.java b/src/main/java/org/zaine/app/dto/CreateCommentDTO.java new file mode 100644 index 0000000..8766e95 --- /dev/null +++ b/src/main/java/org/zaine/app/dto/CreateCommentDTO.java @@ -0,0 +1,50 @@ +package org.zaine.app.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CreateCommentDTO { + + @JsonProperty("page_slug") + private String pageSlug; + + private String author; + + private String content; + + @JsonProperty("parent_id") + private Integer parentId; + + public CreateCommentDTO() {} + + public String getPageSlug() { + return pageSlug; + } + + public void setPageSlug(String pageSlug) { + this.pageSlug = pageSlug; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Integer getParentId() { + return parentId; + } + + public void setParentId(Integer parentId) { + this.parentId = parentId; + } +} diff --git a/src/main/java/org/zaine/app/dto/CreateNoteDTO.java b/src/main/java/org/zaine/app/dto/CreateNoteDTO.java new file mode 100644 index 0000000..8e4896c --- /dev/null +++ b/src/main/java/org/zaine/app/dto/CreateNoteDTO.java @@ -0,0 +1,29 @@ +package org.zaine.app.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CreateNoteDTO { + + @JsonProperty("author_name") + private String authorName; + + private String content; + + public CreateNoteDTO() {} + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/org/zaine/app/model/Comments.java b/src/main/java/org/zaine/app/model/Comments.java new file mode 100644 index 0000000..920327f --- /dev/null +++ b/src/main/java/org/zaine/app/model/Comments.java @@ -0,0 +1,78 @@ +package org.zaine.app.model; + +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "comments") +public class Comments { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @Column(name = "page_slug") + private String pageSlug; + + @Column(name="author") + private String author; + + @Column(name="content") + private String content; + + @JsonProperty("created_at") + @Column(name="created_at") + private Instant createdAt; + + @JsonProperty("parent_id") + @Column(name="parent_id") + private Integer parentId; + + + public Integer getId() { + return id; + } + public void setId(Integer id) { + this.id = id; + } + public String getPageSlug() { + return pageSlug; + } + public void setPageSlug(String pageSlug) { + this.pageSlug = pageSlug; + } + public String getAuthor() { + return author; + } + public void setAuthor(String author) { + this.author = author; + } + public String getContent() { + return content; + } + public void setContent(String content) { + this.content = content; + } + public Instant getCreatedAt() { + return createdAt; + } + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + public Integer getParentId() { + return parentId; + } + public void setParentId(Integer parentId) { + this.parentId = parentId; + } + +} diff --git a/src/main/java/org/zaine/app/model/Competencies.java b/src/main/java/org/zaine/app/model/Competencies.java new file mode 100644 index 0000000..72c59a2 --- /dev/null +++ b/src/main/java/org/zaine/app/model/Competencies.java @@ -0,0 +1,65 @@ +package org.zaine.app.model; + +import java.time.Instant; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; + +@Entity +@Table(name = "competencies") +public class Competencies { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="id") + private Integer id; + + @Column(name="title") + private String title; + + @Column(name="state") + private String state; + + @Column(name="created_at") + private Instant createdAt; + + @Column(name="group_name") + private String group; + + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + +} diff --git a/src/main/java/org/zaine/app/model/Notes.java b/src/main/java/org/zaine/app/model/Notes.java index 4c978b2..c72e277 100644 --- a/src/main/java/org/zaine/app/model/Notes.java +++ b/src/main/java/org/zaine/app/model/Notes.java @@ -9,6 +9,8 @@ import jakarta.persistence.GeneratedValue; import java.time.Instant; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonProperty; + @Entity @Table(name = "public_notes") public class Notes { @@ -21,11 +23,14 @@ public class Notes { @Column(name="content") private String content; + @JsonProperty("author_name") @Column(name="author_name") - private String authorName; + private String authorName; + @JsonProperty("created_at") @Column(name="created_at") - private Instant createdAt; + private Instant createdAt; + public UUID getId() { return id; @@ -51,5 +56,9 @@ public class Notes { return createdAt; } + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + } diff --git a/src/main/java/org/zaine/app/repositories/CommentsRepository.java b/src/main/java/org/zaine/app/repositories/CommentsRepository.java new file mode 100644 index 0000000..8984b27 --- /dev/null +++ b/src/main/java/org/zaine/app/repositories/CommentsRepository.java @@ -0,0 +1,29 @@ +package org.zaine.app.repositories; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.zaine.app.model.Comments; + +public interface CommentsRepository extends JpaRepository { + @Query(value = """ + WITH RECURSIVE comment_tree AS ( + SELECT id, parent_id, author, content, created_at, page_slug + FROM comments + WHERE id = :commentId + AND is_deleted = false + + UNION ALL + + SELECT c.id, c.parent_id, c.author, c.content, c.created_at, c.page_slug + FROM comments c + JOIN comment_tree ct ON c.parent_id = ct.id + WHERE c.is_deleted = false + ) + SELECT * FROM comment_tree + ORDER BY created_at ASC + """, nativeQuery = true) + List findCommentThread(@Param("commentId") Integer commentId); +} diff --git a/src/main/java/org/zaine/app/repositories/CompetenciesRepository.java b/src/main/java/org/zaine/app/repositories/CompetenciesRepository.java new file mode 100644 index 0000000..66afcd7 --- /dev/null +++ b/src/main/java/org/zaine/app/repositories/CompetenciesRepository.java @@ -0,0 +1,9 @@ +package org.zaine.app.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.zaine.app.model.Competencies; + + +public interface CompetenciesRepository extends JpaRepository { + +} diff --git a/src/main/java/org/zaine/app/service/CommentsService.java b/src/main/java/org/zaine/app/service/CommentsService.java new file mode 100644 index 0000000..9b7c022 --- /dev/null +++ b/src/main/java/org/zaine/app/service/CommentsService.java @@ -0,0 +1,54 @@ +package org.zaine.app.service; + +import java.lang.System.Logger; +import java.util.List; +import org.springframework.stereotype.Service; +import org.zaine.app.model.Comments; +import org.zaine.app.repositories.CommentsRepository; +import java.util.stream.*; + +@Service +public class CommentsService { + private static final Logger logger = System.getLogger(CommentsService.class.getName()); + + private final CommentsRepository commentsRepository; + + public CommentsService(CommentsRepository commentsRepository) { + this.commentsRepository = commentsRepository; + } + + public List getAllComments() { + return commentsRepository.findAll(); + } + + public List getAllCommentsBySlug(String page_slug) { + logger.log(System.Logger.Level.INFO, "Entering getAllCommentsBySlug with slug: " + page_slug); + List allComments = commentsRepository.findAll(); + List filteredComments = allComments.stream() + .filter(comment -> page_slug.equals(comment.getPageSlug())) + .collect(Collectors.toList()); + return filteredComments; + } + + public Comments getCommentById(Integer id) { + logger.log(System.Logger.Level.INFO, "Entering getCommentById with id: " + id); + return commentsRepository.findById(id).get(); + } + + public void addComment(Comments comment) { + if (comment == null) { + logger.log(System.Logger.Level.WARNING, "Comment is null. Operation aborted."); + return; + } else { + logger.log(System.Logger.Level.INFO, "Adding comment with id: " + comment.getId()); + commentsRepository.save(comment); + logger.log(System.Logger.Level.INFO, "Added new comment with id: " + comment.getId()); + + } + } + + public List getCommentThread(Integer comment_id) { + logger.log(System.Logger.Level.INFO, "Entering getCommentThread with id: " + comment_id); + return commentsRepository.findCommentThread(comment_id); + } +} diff --git a/src/main/java/org/zaine/app/service/CompetenciesService.java b/src/main/java/org/zaine/app/service/CompetenciesService.java new file mode 100644 index 0000000..5e9ade9 --- /dev/null +++ b/src/main/java/org/zaine/app/service/CompetenciesService.java @@ -0,0 +1,52 @@ +package org.zaine.app.service; + +import java.lang.System.Logger; +import java.util.List; +import org.springframework.stereotype.Service; +import org.zaine.app.model.Competencies; +import org.zaine.app.repositories.CompetenciesRepository; + +@Service +public class CompetenciesService { + + private static final Logger logger = System.getLogger(CompetenciesService.class.getName()); + + private final CompetenciesRepository competenciesRepository; + + private static final List ALLOWED_STATES = + List.of("completed", "manager_review", "in_progress", "not_started", "comments"); + + + public CompetenciesService(CompetenciesRepository competenciesRepository) { + this.competenciesRepository = competenciesRepository; + } + + public List getAllCompetencies() { + return competenciesRepository.findAll(); + + } + + public Competencies getCompetencyById(Integer id) { + + logger.log(System.Logger.Level.INFO, "Entering getCompetencyById with id: " + id); + + return competenciesRepository.findById(id).get(); + } + + + public void updateCompetencyState(Integer id, String newState) { + + if (newState == null) { + throw new IllegalArgumentException("State must not be null"); + } + + if (!ALLOWED_STATES.contains(newState)) { + throw new IllegalArgumentException("Invalid state: " + newState); + } + + Competencies competency = getCompetencyById(id); + competency.setState(newState); + competenciesRepository.save(competency); + } + +} diff --git a/src/test/java/org/zaine/app/service/CompetenciesServiceTest.java b/src/test/java/org/zaine/app/service/CompetenciesServiceTest.java new file mode 100644 index 0000000..0ca2a31 --- /dev/null +++ b/src/test/java/org/zaine/app/service/CompetenciesServiceTest.java @@ -0,0 +1,51 @@ +package org.zaine.app.service; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.zaine.app.repositories.CompetenciesRepository; + +import jakarta.validation.constraints.NotNull; + +import org.zaine.app.model.Competencies; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import java.util.Arrays; +import java.util.Optional; + +@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) +public class CompetenciesServiceTest { + + @InjectMocks + private CompetenciesService competenciesService; + + @Mock + private CompetenciesRepository competenciesRepository; + + @Test + public void testGetAllCompetencies() { + Competencies comp1 = new Competencies(); + comp1.setTitle("Competency 1"); + Competencies comp2 = new Competencies(); + comp2.setTitle("Competency 2"); + when(competenciesRepository.findAll()).thenReturn(Arrays.asList(comp1, comp2)); + + java.util.List result = competenciesService.getAllCompetencies(); + assertEquals(2, result.size()); + assertSame(comp1, result.get(0)); + assertSame(comp2, result.get(1)); + } + + @Test + public void testGetCompetencyByIdReturnsCompetency() { + Competencies comp = new Competencies(); + @NotNull Integer id = comp.getId(); + when(competenciesRepository.findById(id)).thenReturn(Optional.of(comp)); + + + Competencies result = competenciesService.getCompetencyById(id); + assertSame(comp, result); + + } +}